source: src/mlx/fsuipc.py@ 1089:03726a21f686

python3
Last change on this file since 1089:03726a21f686 was 1089:03726a21f686, checked in by István Váradi <ivaradi@…>, 13 months ago

FSUIPC offset 0x0590 is used to query altitude on MSFS 2020 (re #366)

File size: 88.1 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
576 TIME_SYNC_INTERVAL = 3.0
577
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
586 timestamp += data[4]
587
588 return timestamp
589
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))
602
603 data.append((offset + 3, "b", 0))
604
605 def __init__(self, connectionListener, connectAttempts = -1,
606 connectInterval = 0.2):
607 """Construct the simulator.
608
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.
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
619 time.
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."""
627 self._fsType = None
628 self._aircraft = None
629
630 self._handler = Handler(self,
631 connectAttempts = connectAttempts,
632 connectInterval = connectInterval)
633 self._connectionListener = connectionListener
634 self._handler.start()
635
636 self._scroll = False
637
638 self._syncTime = False
639 self._nextSyncTime = -1
640
641 self._normalRequestID = None
642
643 self._monitoringRequested = False
644 self._monitoring = False
645
646 self._aircraftName = None
647 self._aircraftModel = None
648
649 self._flareRequestID = None
650 self._flareRates = []
651 self._flareStart = None
652 self._flareStartFS = None
653
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
662 self._fuelCallback = None
663
664 def connect(self, aircraft):
665 """Initiate a connection to the simulator."""
666 self._aircraft = aircraft
667 self._aircraftName = None
668 self._aircraftModel = None
669 self._handler.connect()
670 if self._normalRequestID is None:
671 self._nextSyncTime = -1
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)
684
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
689 separate arguments."""
690 self._handler.requestRead([(0x13fc, "d")], self._handlePayloadCount,
691 extra = callback)
692
693 def requestTime(self, callback):
694 """Request the time from the simulator."""
695 self._handler.requestRead(Simulator.timeData, self._handleTime,
696 extra = callback)
697
698 def startMonitoring(self):
699 """Start the periodic monitoring of the aircraft and pass the resulting
700 state to the aircraft object periodically."""
701 assert not self._monitoringRequested
702 self._monitoringRequested = True
703
704 def stopMonitoring(self):
705 """Stop the periodic monitoring of the aircraft."""
706 assert self._monitoringRequested
707 self._monitoringRequested = False
708
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
730 def sendMessage(self, message, duration = 3,
731 _disconnect = False):
732 """Send a message to the pilot via the simulator.
733
734 duration is the number of seconds to keep the message displayed."""
735
736 print("fsuipc.Simulator.sendMessage:", message)
737
738 if self._scroll:
739 if duration==0: duration = -1
740 elif duration == 1: duration = -2
741 else: duration = -duration
742
743 try:
744 message = bytes(str(message), "iso-8859-1")
745 except Exception as e:
746 print("fsuipc.Simulator.sendMessage: failed to convert the message to a string:", e)
747
748 data = [(0x3380, -1 - len(message), message),
749 (0x32fa, 'h', duration)]
750
751 #if _disconnect:
752 # print "fsuipc.Simulator.sendMessage(disconnect)", message
753
754 self._handler.requestWrite(data, self._handleMessageSent,
755 extra = _disconnect,
756 unimportant = True)
757
758 def getFuel(self, callback):
759 """Get the fuel information for the current model.
760
761 The callback will be called with a list of triplets with the following
762 items:
763 - the fuel tank identifier
764 - the current weight of the fuel in the tank (in kgs)
765 - the current total capacity of the tank (in kgs)."""
766 if self._aircraftModel is None:
767 self._fuelCallback = callback
768 else:
769 self._aircraftModel.getFuel(self._handler, callback)
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."""
779 if self._aircraftModel is not None:
780 self._aircraftModel.setFuelLevel(self._handler, levels)
781
782 def enableTimeSync(self):
783 """Enable the time synchronization."""
784 self._nextSyncTime = -1
785 self._syncTime = True
786
787 def disableTimeSync(self):
788 """Enable the time synchronization."""
789 self._syncTime = False
790 self._nextSyncTime = -1
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
805
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()
830
831 def disconnect(self, closingMessage = None, duration = 3):
832 """Disconnect from the simulator."""
833 assert not self._monitoringRequested
834
835 print("fsuipc.Simulator.disconnect", closingMessage, duration)
836
837 self._stopNormal()
838 self.clearHotkeys()
839 if closingMessage is None:
840 self._handler.disconnect()
841 else:
842 self.sendMessage(closingMessage, duration = duration,
843 _disconnect = True)
844
845 def connected(self, fsType, descriptor):
846 """Called when a connection has been established to the flight
847 simulator of the given type."""
848 self._fsType = fsType
849 with self._hotkeyLock:
850 if self._hotkeys is not None:
851 self._hotkeySetGeneration += 1
852
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
871 def _startDefaultNormal(self):
872 """Start the default normal periodic request."""
873 assert self._normalRequestID is None
874 self._normalRequestID = \
875 self._handler.requestPeriodicRead(1.0,
876 Simulator.normalData,
877 self._handleNormal,
878 validator = self._validateNormal)
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
885 self._monitoring = False
886
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
891 def _handleNormal(self, data, extra):
892 """Handle the reply to the normal request.
893
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.
897 """
898 timestamp = Simulator._getTimestamp(data)
899
900 aircraftName = str(data[5], "iso-8859-1")
901 aircraftPath = str(data[6], "iso-8859-1")
902
903 createdNewModel = self._setAircraftName(timestamp, aircraftName, aircraftPath)
904 if self._fuelCallback is not None:
905 self._aircraftModel.getFuel(self._handler, self._fuelCallback)
906 self._fuelCallback = None
907
908 self._scroll = data[7]!=0
909
910 if self._monitoringRequested and not self._monitoring:
911 self._stopNormal()
912 self._startMonitoring()
913 elif self._monitoring and not self._monitoringRequested:
914 self._stopNormal()
915 self._startDefaultNormal()
916 elif self._monitoring and self._aircraftModel is not None and \
917 not createdNewModel:
918 aircraftState = self._aircraftModel.getAircraftState(self._aircraft,
919 timestamp, data)
920
921 self._checkTimeSync(aircraftState)
922
923 self._aircraft.handleState(aircraftState)
924
925 def _checkTimeSync(self, aircraftState):
926 """Check if we need to synchronize the FS time."""
927 if not self._syncTime or aircraftState.paused or \
928 self._flareRequestID is not None:
929 self._nextSyncTime = -1
930 return
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
938
939 self._handler.requestWrite([(0x023a, "b", int(seconds))],
940 self._handleTimeSynced)
941
942 #print "Set the seconds to ", seconds
943
944 if self._nextSyncTime<0:
945 self._nextSyncTime = now
946
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
954
955 def _setAircraftName(self, timestamp, name, airPath):
956 """Set the name of the aicraft and if it is different from the
957 previous, create a new model for it.
958
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
965
966 print("fsuipc.Simulator: new aircraft name and air file path: %s, %s" % \
967 (name, airPath))
968
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:
979 self._setAircraftModel(AircraftModel.create(self._aircraft,
980 aircraftName))
981
982 self._aircraft.modelChanged(timestamp, name, self._aircraftModel.name)
983
984 return needNew
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
992 model.setFSType(self._fsType)
993
994 if self._monitoring:
995 self._stopNormal()
996 self._startMonitoring()
997
998 def _startMonitoring(self):
999 """Start monitoring with the current aircraft model."""
1000 data = Simulator.normalData[:]
1001 self._aircraftModel.addMonitoringData(data, self._fsType)
1002
1003 self._normalRequestID = \
1004 self._handler.requestPeriodicRead(1.0, data,
1005 self._handleNormal,
1006 validator = self._validateNormal)
1007 self._monitoring = True
1008
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 = \
1023 self._handler.requestPeriodicRead(0.1,
1024 Simulator.flareData2,
1025 self._handleFlare2)
1026 self._handler.requestRead(Simulator.flareStartData,
1027 self._handleFlareStart)
1028
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])
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)
1073
1074 def _handleTime(self, data, callback):
1075 """Callback for a time retrieval request."""
1076 callback(Simulator._getTimestamp(data))
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"))
1084
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)
1095
1096 def _handleMessageSent(self, success, disconnect):
1097 """Callback for a message sending request."""
1098 #print "fsuipc.Simulator._handleMessageSent", disconnect
1099 if disconnect:
1100 self._handler.disconnect()
1101
1102 def _handleNumHotkeys(self, data, hotkeySet):
1103 """Handle the result of the query of the number of hotkeys"""
1104 (id, generation) = hotkeySet
1105 with self._hotkeyLock:
1106 if id==self._hotkeySetID and generation==self._hotkeySetGeneration:
1107 numHotkeys = data[0]
1108 print("fsuipc.Simulator._handleNumHotkeys: numHotkeys:", numHotkeys)
1109 data = [(0x3210 + i*4, "d") for i in range(0, numHotkeys)]
1110 self._handler.requestRead(data, self._handleHotkeyTable,
1111 hotkeySet)
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:
1150 writeData.append((offset, "u", int(0)))
1151
1152 return writeData
1153
1154 def _handleHotkeyTable(self, data, hotkeySet):
1155 """Handle the result of the query of the hotkey table."""
1156 (id, generation) = hotkeySet
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,
1162 hotkeySet)
1163
1164 def _handleHotkeysWritten(self, success, hotkeySet):
1165 """Handle the result of the hotkeys having been written."""
1166 (id, generation) = hotkeySet
1167 with self._hotkeyLock:
1168 if success and id==self._hotkeySetID and \
1169 generation==self._hotkeySetGeneration:
1170 data = [(offset + 3, "b") for offset in self._hotkeyOffets]
1171
1172 self._hotkeyRequestID = \
1173 self._handler.requestPeriodicRead(0.5, data,
1174 self._handleHotkeys,
1175 hotkeySet)
1176
1177 def _handleHotkeys(self, data, hotkeySet):
1178 """Handle the hotkeys."""
1179 (id, generation) = hotkeySet
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):
1201 """Callback for the hotkey-clearing write request."""
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
1208
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."""
1216 monitoringData = [("paused", 0x0264, "H"),
1217 ("latitude", 0x0560, "l"),
1218 ("longitude", 0x0568, "l"),
1219 ("frozen", 0x3364, "H"),
1220 ("replay", 0x0628, "d"),
1221 ("slew", 0x05dc, "H"),
1222 ("overspeed", 0x036d, "b"),
1223 ("stalled", 0x036c, "b"),
1224 ("onTheGround", 0x0366, "H"),
1225 ("zfw", 0x3bfc, "d"),
1226 ("grossWeight", 0x30c0, "f"),
1227 ("heading", 0x0580, "d"),
1228 ("pitch", 0x0578, "d"),
1229 ("bank", 0x057c, "d"),
1230 ("ias", 0x02bc, "d"),
1231 ("mach", 0x11c6, "H"),
1232 ("groundSpeed", 0x02b4, "d"),
1233 ("vs", 0x02c8, "d"),
1234 ("radioAltitude", 0x31e4, "d"),
1235 ("altitude", 0x0570, "l"),
1236 ("msfs2020Altitude", 0x0590, "F"),
1237 ("gLoad", 0x11ba, "h"),
1238 ("flapsControl", 0x0bdc, "d"),
1239 ("flapsLeft", 0x0be0, "d"),
1240 ("flapsRight", 0x0be4, "d"),
1241 ("flapsAxis", 0x3414, "H"),
1242 ("flapsIncrement", 0x3bfa, "H"),
1243 ("lights", 0x0d0c, "H"),
1244 ("pitot", 0x029c, "b"),
1245 ("parking", 0x0bc8, "H"),
1246 ("gearControl", 0x0be8, "d"),
1247 ("noseGear", 0x0bec, "d"),
1248 ("spoilersArmed", 0x0bcc, "d"),
1249 ("spoilers", 0x0bd0, "d"),
1250 ("altimeter", 0x0330, "H"),
1251 ("qnh", 0x0ec6, "H"),
1252 ("nav1", 0x0350, "H"),
1253 ("nav1_obs", 0x0c4e, "H"),
1254 ("nav2", 0x0352, "H"),
1255 ("nav2_obs", 0x0c5e, "H"),
1256 ("adf1_main", 0x034c, "H"),
1257 ("adf1_ext", 0x0356, "H"),
1258 ("adf2_main", 0x02d4, "H"),
1259 ("adf2_ext", 0x02d6, "H"),
1260 ("squawk", 0x0354, "H"),
1261 ("windSpeed", 0x0e90, "H"),
1262 ("windDirection", 0x0e92, "H"),
1263 ("visibility", 0x0e8a, "H"),
1264 ("cog", 0x2ef8, "f"),
1265 ("xpdrC", 0x7b91, "b"),
1266 ("apMaster", 0x07bc, "d"),
1267 ("apHeadingHold", 0x07c8, "d"),
1268 ("apHeading", 0x07cc, "H"),
1269 ("apAltitudeHold", 0x07d0, "d"),
1270 ("apAltitude", 0x07d4, "u"),
1271 ("elevatorTrim", 0x2ea0, "f"),
1272 ("eng1DeIce", 0x08b2, "H"),
1273 ("eng2DeIce", 0x094a, "H"),
1274 ("propDeIce", 0x337c, "b"),
1275 ("structDeIce", 0x337d, "b")]
1276
1277 specialModels = []
1278
1279 @staticmethod
1280 def registerSpecial(clazz):
1281 """Register the given class as a special model."""
1282 AircraftModel.specialModels.append(clazz)
1283
1284 @staticmethod
1285 def findSpecial(aircraft, aircraftName):
1286 for specialModel in AircraftModel.specialModels:
1287 if specialModel.doesHandle(aircraft, aircraftName):
1288 return specialModel
1289 return None
1290
1291 @staticmethod
1292 def create(aircraft, aircraftName):
1293 """Create the model for the given aircraft name, and notify the
1294 aircraft about it."""
1295 specialModel = AircraftModel.findSpecial(aircraft, aircraftName)
1296 if specialModel is not None:
1297 return specialModel()
1298 if aircraft.type in _genericModels:
1299 return _genericModels[aircraft.type]()
1300 else:
1301 return GenericModel()
1302
1303 @staticmethod
1304 def convertBCD(data, length):
1305 """Convert a data item encoded as BCD into a string of the given number
1306 of digits."""
1307 bcd = ""
1308 for i in range(0, length):
1309 digit = chr(ord('0') + (data&0x0f))
1310 data >>= 4
1311 bcd = digit + bcd
1312 return bcd
1313
1314 @staticmethod
1315 def convertFrequency(data):
1316 """Convert the given frequency data to a string."""
1317 bcd = AircraftModel.convertBCD(data, 4)
1318 return "1" + bcd[0:2] + "." + bcd[2:4]
1319
1320 @staticmethod
1321 def convertADFFrequency(main, ext):
1322 """Convert the given ADF frequency data to a string."""
1323 mainBCD = AircraftModel.convertBCD(main, 4)
1324 extBCD = AircraftModel.convertBCD(ext, 4)
1325
1326 return (extBCD[1] if extBCD[1]!="0" else "") + \
1327 mainBCD[1:] + "." + extBCD[3]
1328
1329 def __init__(self, flapsNotches):
1330 """Construct the aircraft model.
1331
1332 flapsNotches is a list of degrees of flaps that are available on the aircraft."""
1333 self._flapsNotches = flapsNotches
1334 self._xpdrReliable = False
1335 self._flapsSet = -1
1336 self._fsType = None
1337
1338 @property
1339 def name(self):
1340 """Get the name for this aircraft model."""
1341 return "FSUIPC/Generic"
1342
1343 def setFSType(self, fsType):
1344 """Set the flight simulator type."""
1345 self._fsType = fsType
1346
1347 def doesHandle(self, aircraft, aircraftName):
1348 """Determine if the model handles the given aircraft name.
1349
1350 This default implementation returns False."""
1351 return False
1352
1353 def _addOffsetWithIndexMember(self, dest, offset, type, attrName = None):
1354 """Add the given FSUIPC offset and type to the given array and a member
1355 attribute with the given name."""
1356 dest.append((offset, type))
1357 if attrName is not None:
1358 setattr(self, attrName, len(dest)-1)
1359
1360 def _addDataWithIndexMembers(self, dest, prefix, data):
1361 """Add FSUIPC data to the given array and also corresponding index
1362 member variables with the given prefix.
1363
1364 data is a list of triplets of the following items:
1365 - the name of the data item. The index member variable will have a name
1366 created by prepending the given prefix to this name.
1367 - the FSUIPC offset
1368 - the FSUIPC type
1369
1370 The latter two items will be appended to dest."""
1371 for (name, offset, type) in data:
1372 self._addOffsetWithIndexMember(dest, offset, type, prefix + name)
1373
1374 def addMonitoringData(self, data, fsType):
1375 """Add the model-specific monitoring data to the given array."""
1376 self._addDataWithIndexMembers(data, "_monidx_",
1377 AircraftModel.monitoringData)
1378
1379 def getAircraftState(self, aircraft, timestamp, data):
1380 """Get an aircraft state object for the given monitoring data."""
1381 state = fs.AircraftState()
1382
1383 state.timestamp = timestamp
1384
1385 state.latitude = data[self._monidx_latitude] * \
1386 90.0 / 10001750.0 / 65536.0 / 65536.0
1387
1388 state.longitude = data[self._monidx_longitude] * \
1389 360.0 / 65536.0 / 65536.0 / 65536.0 / 65536.0
1390 if state.longitude>180.0: state.longitude = 360.0 - state.longitude
1391
1392 state.paused = data[self._monidx_paused]!=0 or \
1393 data[self._monidx_frozen]!=0 or \
1394 data[self._monidx_replay]!=0
1395 state.trickMode = data[self._monidx_slew]!=0
1396
1397 state.overspeed = data[self._monidx_overspeed]!=0
1398 state.stalled = data[self._monidx_stalled]!=0
1399 state.onTheGround = data[self._monidx_onTheGround]!=0
1400
1401 state.zfw = data[self._monidx_zfw] * const.LBSTOKG / 256.0
1402 state.grossWeight = data[self._monidx_grossWeight] * const.LBSTOKG
1403
1404 state.heading = Handler.fsuipc2PositiveDegrees(data[self._monidx_heading])
1405
1406 state.pitch = Handler.fsuipc2Degrees(data[self._monidx_pitch])
1407 state.bank = Handler.fsuipc2Degrees(data[self._monidx_bank])
1408
1409 state.ias = Handler.fsuipc2IAS(data[self._monidx_ias])
1410 state.mach = data[self._monidx_mach] / 20480.0
1411 state.groundSpeed = data[self._monidx_groundSpeed]* 3600.0/65536.0/1852.0
1412 state.vs = Handler.fsuipc2VS(data[self._monidx_vs])
1413
1414 state.radioAltitude = \
1415 Handler.fsuipc2radioAltitude(data[self._monidx_radioAltitude])
1416 if self._fsType==const.SIM_MSFS2020:
1417 state.altitude = data[self._monidx_msfs2020Altitude]
1418 else:
1419 state.altitude = data[self._monidx_altitude]/const.FEETTOMETRES/65536.0/65536.0
1420
1421 state.gLoad = data[self._monidx_gLoad] / 625.0
1422
1423 numNotchesM1 = len(self._flapsNotches) - 1
1424 flapsIncrement = 16383 // numNotchesM1
1425 flapsControl = data[self._monidx_flapsControl]
1426 flapsIndex = flapsControl // flapsIncrement
1427 if flapsIndex < numNotchesM1:
1428 if (flapsControl - (flapsIndex*flapsIncrement) >
1429 (flapsIndex+1)*flapsIncrement - flapsControl):
1430 flapsIndex += 1
1431 state.flapsSet = self._flapsNotches[flapsIndex]
1432 if state.flapsSet != self._flapsSet:
1433 print("flapsControl: %d, flapsLeft: %d, flapsRight: %d, flapsAxis: %d, flapsIncrement: %d, flapsSet: %d, numNotchesM1: %d" % \
1434 (flapsControl, data[self._monidx_flapsLeft],
1435 data[self._monidx_flapsRight], data[self._monidx_flapsAxis],
1436 data[self._monidx_flapsIncrement], state.flapsSet, numNotchesM1))
1437 self._flapsSet = state.flapsSet
1438
1439 flapsLeft = data[self._monidx_flapsLeft]
1440 state.flaps = self._flapsNotches[-1]*flapsLeft/16383.0
1441
1442 lights = data[self._monidx_lights]
1443
1444 state.navLightsOn = (lights&0x01) != 0
1445 state.antiCollisionLightsOn = (lights&0x02) != 0
1446 state.landingLightsOn = (lights&0x04) != 0
1447 state.strobeLightsOn = (lights&0x10) != 0
1448
1449 state.pitotHeatOn = data[self._monidx_pitot]!=0
1450
1451 state.parking = data[self._monidx_parking]!=0
1452
1453 state.gearControlDown = data[self._monidx_gearControl]==16383
1454 state.gearsDown = data[self._monidx_noseGear]==16383
1455
1456 state.spoilersArmed = data[self._monidx_spoilersArmed]!=0
1457
1458 spoilers = data[self._monidx_spoilers]
1459 if spoilers<=4800:
1460 state.spoilersExtension = 0.0
1461 else:
1462 state.spoilersExtension = (spoilers - 4800) * 100.0 / (16383 - 4800)
1463
1464 state.altimeter = data[self._monidx_altimeter] / 16.0
1465 state.altimeterReliable = True
1466 state.qnh = data[self._monidx_qnh] / 16.0
1467
1468 state.ils = None
1469 state.ils_obs = None
1470 state.ils_manual = False
1471 state.nav1 = AircraftModel.convertFrequency(data[self._monidx_nav1])
1472 state.nav1_obs = data[self._monidx_nav1_obs]
1473 state.nav1_manual = True
1474 state.nav2 = AircraftModel.convertFrequency(data[self._monidx_nav2])
1475 state.nav2_obs = data[self._monidx_nav2_obs]
1476 state.nav2_manual = True
1477 state.adf1 = \
1478 AircraftModel.convertADFFrequency(data[self._monidx_adf1_main],
1479 data[self._monidx_adf1_ext])
1480 state.adf2 = \
1481 AircraftModel.convertADFFrequency(data[self._monidx_adf2_main],
1482 data[self._monidx_adf2_ext])
1483
1484 state.squawk = AircraftModel.convertBCD(data[self._monidx_squawk], 4)
1485
1486 state.windSpeed = data[self._monidx_windSpeed]
1487 state.windDirection = data[self._monidx_windDirection]*360.0/65536.0
1488 if state.windDirection<0.0: state.windDirection += 360.0
1489
1490 state.visibility = data[self._monidx_visibility]*1609.344/100.0
1491
1492 state.cog = data[self._monidx_cog]
1493
1494 if not self._xpdrReliable:
1495 self._xpdrReliable = data[self._monidx_xpdrC]!=0
1496
1497 state.xpdrC = data[self._monidx_xpdrC]!=1 \
1498 if self._xpdrReliable else None
1499 state.autoXPDR = False
1500
1501 state.apMaster = data[self._monidx_apMaster]!=0
1502 state.apHeadingHold = data[self._monidx_apHeadingHold]!=0
1503 state.apHeading = data[self._monidx_apHeading] * 360.0 / 65536.0
1504 state.apAltitudeHold = data[self._monidx_apAltitudeHold]!=0
1505 state.apAltitude = data[self._monidx_apAltitude] / \
1506 const.FEETTOMETRES / 65536.0
1507
1508
1509 state.elevatorTrim = data[self._monidx_elevatorTrim] * 180.0 / math.pi
1510
1511 state.antiIceOn = data[self._monidx_eng1DeIce]!=0 or \
1512 data[self._monidx_eng2DeIce]!=0 or \
1513 data[self._monidx_propDeIce]!=0 or \
1514 data[self._monidx_structDeIce]!=0
1515
1516 return state
1517
1518#------------------------------------------------------------------------------
1519
1520class GenericAircraftModel(AircraftModel):
1521 """A generic aircraft model that can handle the fuel levels, the N1 or RPM
1522 values and some other common parameters in a generic way."""
1523
1524 def __init__(self, flapsNotches, fuelTanks, numEngines, isN1 = True):
1525 """Construct the generic aircraft model with the given data.
1526
1527 flapsNotches is an array of how much degrees the individual flaps
1528 notches mean.
1529
1530 fuelTanks is an array of const.FUELTANK_XXX constants about the
1531 aircraft's fuel tanks. They will be converted to offsets.
1532
1533 numEngines is the number of engines the aircraft has.
1534
1535 isN1 determines if the engines have an N1 value or an RPM value
1536 (e.g. pistons)."""
1537 super(GenericAircraftModel, self).__init__(flapsNotches = flapsNotches)
1538
1539 self._fuelTanks = fuelTanks
1540 self._fuelStartIndex = None
1541 self._numEngines = numEngines
1542 self._engineStartIndex = None
1543 self._isN1 = isN1
1544
1545 def doesHandle(self, aircraft, aircraftName):
1546 """Determine if the model handles the given aircraft name.
1547
1548 This implementation returns True."""
1549 return True
1550
1551 def addMonitoringData(self, data, fsType):
1552 """Add the model-specific monitoring data to the given array."""
1553 super(GenericAircraftModel, self).addMonitoringData(data, fsType)
1554
1555 self._fuelStartIndex = self._addFuelOffsets(data, "_monidx_fuelWeight")
1556
1557 self._engineStartIndex = len(data)
1558 for i in range(0, self._numEngines):
1559 self._addOffsetWithIndexMember(data, 0x088c + i * 0x98, "h") # throttle lever
1560 if self._isN1:
1561 self._addOffsetWithIndexMember(data, 0x2000 + i * 0x100, "f") # N1
1562 else:
1563 self._addOffsetWithIndexMember(data, 0x0898 + i * 0x98, "H") # RPM
1564 self._addOffsetWithIndexMember(data, 0x08c8 + i * 0x98, "H") # RPM scaler
1565
1566 def getAircraftState(self, aircraft, timestamp, data):
1567 """Get the aircraft state.
1568
1569 Get it from the parent, and then add the data about the fuel levels and
1570 the engine parameters."""
1571 state = super(GenericAircraftModel, self).getAircraftState(aircraft,
1572 timestamp,
1573 data)
1574
1575 (state.fuel, state.totalFuel) = \
1576 self._convertFuelData(data, index = self._monidx_fuelWeight)
1577
1578 state.n1 = [] if self._isN1 else None
1579 state.rpm = None if self._isN1 else []
1580 itemsPerEngine = 2 if self._isN1 else 3
1581
1582 state.reverser = []
1583 for i in range(self._engineStartIndex,
1584 self._engineStartIndex +
1585 itemsPerEngine*self._numEngines,
1586 itemsPerEngine):
1587 state.reverser.append(data[i]<0)
1588 if self._isN1:
1589 state.n1.append(data[i+1])
1590 else:
1591 state.rpm.append(data[i+1] * data[i+2]/65536.0)
1592
1593 return state
1594
1595 def getFuel(self, handler, callback):
1596 """Get the fuel information for this model.
1597
1598 See Simulator.getFuel for more information. This
1599 implementation simply queries the fuel tanks given to the
1600 constructor."""
1601 data = []
1602 self._addFuelOffsets(data)
1603
1604 handler.requestRead(data, self._handleFuelRetrieved,
1605 extra = callback)
1606
1607 def setFuelLevel(self, handler, levels):
1608 """Set the fuel level.
1609
1610 See the description of Simulator.setFuelLevel. This
1611 implementation simply sets the fuel tanks as given."""
1612 data = []
1613 for (tank, level) in levels:
1614 offset = _tank2offset[tank]
1615 value = int(level * 128.0 * 65536.0)
1616 data.append( (offset, "u", value) )
1617
1618 handler.requestWrite(data, self._handleFuelWritten)
1619
1620 def _addFuelOffsets(self, data, weightIndexName = None):
1621 """Add the fuel offsets to the given data array.
1622
1623 If weightIndexName is not None, it will be the name of the
1624 fuel weight index.
1625
1626 Returns the index of the first fuel tank's data."""
1627 self._addOffsetWithIndexMember(data, 0x0af4, "H", weightIndexName)
1628
1629 fuelStartIndex = len(data)
1630 for tank in self._fuelTanks:
1631 offset = _tank2offset[tank]
1632 self._addOffsetWithIndexMember(data, offset, "u") # tank level
1633 self._addOffsetWithIndexMember(data, offset+4, "u") # tank capacity
1634
1635 return fuelStartIndex
1636
1637 def _convertFuelData(self, data, index = 0, addCapacities = False):
1638 """Convert the given data into a fuel info list.
1639
1640 The list consists of two or three-tuples of the following
1641 items:
1642 - the fuel tank ID,
1643 - the amount of the fuel in kg,
1644 - if addCapacities is True, the total capacity of the tank."""
1645 fuelWeight = data[index] / 256.0
1646 index += 1
1647
1648 result = []
1649 totalFuel = 0
1650 for fuelTank in self._fuelTanks:
1651 capacity = data[index+1] * fuelWeight * const.LBSTOKG
1652 if capacity>=1.0:
1653 amount = data[index] * capacity / 128.0 / 65536.0
1654
1655 result.append( (fuelTank, amount, capacity) if addCapacities
1656 else (fuelTank, amount))
1657 totalFuel += amount
1658 index += 2
1659
1660 return (result, totalFuel)
1661
1662 def _handleFuelRetrieved(self, data, callback):
1663 """Callback for a fuel retrieval request."""
1664 (fuelData, _totalFuel) = self._convertFuelData(data,
1665 addCapacities = True)
1666 callback(fuelData)
1667
1668 def _handleFuelWritten(self, success, extra):
1669 """Callback for a fuel setting request."""
1670 pass
1671
1672#------------------------------------------------------------------------------
1673
1674class GenericModel(GenericAircraftModel):
1675 """Generic aircraft model for an unknown type."""
1676 def __init__(self):
1677 """Construct the model."""
1678 super(GenericModel, self). \
1679 __init__(flapsNotches = [0, 10, 20, 30],
1680 fuelTanks = [const.FUELTANK_LEFT, const.FUELTANK_RIGHT],
1681 numEngines = 2)
1682
1683 @property
1684 def name(self):
1685 """Get the name for this aircraft model."""
1686 return "FSUIPC/Generic"
1687
1688#------------------------------------------------------------------------------
1689
1690class B737Model(GenericAircraftModel):
1691 """Generic model for the Boeing 737 Classing and NG aircraft."""
1692 fuelTanks = [const.FUELTANK_LEFT, const.FUELTANK_CENTRE, const.FUELTANK_RIGHT]
1693
1694 def __init__(self):
1695 """Construct the model."""
1696 super(B737Model, self). \
1697 __init__(flapsNotches = [0, 1, 2, 5, 10, 15, 25, 30, 40],
1698 fuelTanks = B737Model.fuelTanks,
1699 numEngines = 2)
1700
1701 @property
1702 def name(self):
1703 """Get the name for this aircraft model."""
1704 return "FSUIPC/Generic Boeing 737"
1705
1706 # Note: the function below should be enabled if testing the speed-based
1707 # takeoff on Linux
1708 # def getAircraftState(self, aircraft, timestamp, data):
1709 # """Get the aircraft state.
1710
1711 # Get it from the parent, and then check some PMDG-specific stuff."""
1712 # state = super(B737Model, self).getAircraftState(aircraft,
1713 # timestamp,
1714 # data)
1715 # state.strobeLightsOn = None
1716 # state.xpdrC = None
1717
1718 # return state
1719
1720#------------------------------------------------------------------------------
1721
1722class PMDGBoeing737NGModel(B737Model):
1723 """A model handler for the PMDG Boeing 737NG model."""
1724 @staticmethod
1725 def doesHandle(aircraft, aircraftName):
1726 """Determine if this model handler handles the aircraft with the given
1727 name."""
1728 (name, airPath) = aircraftName
1729 return aircraft.type in [const.AIRCRAFT_B736,
1730 const.AIRCRAFT_B737,
1731 const.AIRCRAFT_B738,
1732 const.AIRCRAFT_B738C] and \
1733 (name.find("PMDG")!=-1 or airPath.find("PMDG")!=-1) and \
1734 (name.find("737")!=-1 or airPath.find("737")!=-1) and \
1735 (name.find("600")!=-1 or airPath.find("600")!=-1 or \
1736 name.find("700")!=-1 or airPath.find("700")!=-1 or \
1737 name.find("800")!=-1 or airPath.find("800")!=-1 or \
1738 name.find("900")!=-1 or airPath.find("900")!=-1)
1739
1740 def __init__(self):
1741 """Construct the model."""
1742 super(PMDGBoeing737NGModel, self).__init__()
1743 self._lastGearControl = None
1744 self._lastNoseGear = None
1745
1746 @property
1747 def name(self):
1748 """Get the name for this aircraft model."""
1749 return "FSUIPC/PMDG Boeing 737NG(X)"
1750
1751 def addMonitoringData(self, data, fsType):
1752 """Add the model-specific monitoring data to the given array."""
1753 super(PMDGBoeing737NGModel, self).addMonitoringData(data, fsType)
1754
1755 if fsType==const.SIM_MSFSX or fsType==const.SIM_P3D or fsType==const.SIM_MSFS2020:
1756 print("%s detected, adding PMDG 737 NGX-specific offsets" % \
1757 ("FSX" if fsType==const.SIM_MSFSX else
1758 "P3D" if fsType==const.SIM_P3D else "MSFS 2020",))
1759 self._addOffsetWithIndexMember(data, 0x6500, "b",
1760 "_pmdgidx_lts_positionsw")
1761 self._addOffsetWithIndexMember(data, 0x6545, "b", "_pmdgidx_cmda")
1762 self._addOffsetWithIndexMember(data, 0x653f, "b", "_pmdgidx_aphdgsel")
1763 self._addOffsetWithIndexMember(data, 0x6543, "b", "_pmdgidx_apalthold")
1764 self._addOffsetWithIndexMember(data, 0x652c, "H", "_pmdgidx_aphdg")
1765 self._addOffsetWithIndexMember(data, 0x652e, "H", "_pmdgidx_apalt")
1766 if fsType==const.SIM_MSFS2020:
1767 self._addOffsetWithIndexMember(data, 0x0b46, "b", "_pmdgidx_xpdr")
1768 else:
1769 self._addOffsetWithIndexMember(data, 0x65cd, "b", "_pmdgidx_xpdr")
1770 else:
1771 print("FS9 detected, adding PMDG 737 NG-specific offsets")
1772 self._addOffsetWithIndexMember(data, 0x6202, "b", "_pmdgidx_switches")
1773 self._addOffsetWithIndexMember(data, 0x6216, "b", "_pmdgidx_xpdr")
1774 self._addOffsetWithIndexMember(data, 0x6227, "b", "_pmdgidx_ap")
1775 self._addOffsetWithIndexMember(data, 0x6228, "b", "_pmdgidx_aphdgsel")
1776 self._addOffsetWithIndexMember(data, 0x622a, "b", "_pmdgidx_apalthold")
1777 self._addOffsetWithIndexMember(data, 0x622c, "H", "_pmdgidx_aphdg")
1778 self._addOffsetWithIndexMember(data, 0x622e, "H", "_pmdgidx_apalt")
1779
1780 def getAircraftState(self, aircraft, timestamp, data):
1781 """Get the aircraft state.
1782
1783 Get it from the parent, and then check some PMDG-specific stuff."""
1784 state = super(PMDGBoeing737NGModel, self).getAircraftState(aircraft,
1785 timestamp,
1786 data)
1787
1788 fsType = self._fsType
1789 if fsType==const.SIM_MSFSX or fsType==const.SIM_P3D or \
1790 fsType==const.SIM_MSFS2020:
1791 state.apMaster = data[self._pmdgidx_cmda]!=0
1792 state.apHeadingHold = data[self._pmdgidx_aphdgsel]!=0
1793 state.apAltitudeHold = data[self._pmdgidx_apalthold]!=0
1794
1795 # state.strobeLightsOn = data[self._pmdgidx_lts_positionsw]==0x02
1796 # state.xpdrC = data[self._pmdgidx_xpdr]==4
1797 if fsType==const.SIM_MSFS2020:
1798 state.xpdrC = data[self._pmdgidx_xpdr]==4
1799 else:
1800 state.strobeLightsOn = None
1801 state.xpdrC = None
1802 else:
1803 if data[self._pmdgidx_switches]&0x01==0x01:
1804 state.altimeter = 1013.25
1805 state.apMaster = data[self._pmdgidx_ap]&0x02==0x02
1806 state.apHeadingHold = data[self._pmdgidx_aphdgsel]==2
1807 apalthold = data[self._pmdgidx_apalthold]
1808 state.apAltitudeHold = apalthold>=3 and apalthold<=6
1809 state.xpdrC = data[self._pmdgidx_xpdr]==4
1810
1811 # Uncomment the following to test the speed-based takeoff
1812 # state.strobeLightsOn = None
1813 # state.xpdrC = None
1814
1815 state.apHeading = data[self._pmdgidx_aphdg]
1816 state.apAltitude = data[self._pmdgidx_apalt]
1817
1818 gearControl = data[self._monidx_gearControl]
1819 noseGear = data[self._monidx_noseGear]
1820
1821 if gearControl!=self._lastGearControl or noseGear!=self._lastNoseGear:
1822 print("gearControl:", gearControl, " noseGear:", noseGear)
1823 self._lastGearControl = gearControl
1824 self._lastNoseGear = noseGear
1825
1826 return state
1827
1828#------------------------------------------------------------------------------
1829
1830class B767Model(GenericAircraftModel):
1831 """Generic model for the Boeing 767 aircraft."""
1832 fuelTanks = [const.FUELTANK_LEFT, const.FUELTANK_CENTRE, const.FUELTANK_RIGHT]
1833
1834 def __init__(self):
1835 """Construct the model."""
1836 super(B767Model, self). \
1837 __init__(flapsNotches = [0, 1, 5, 15, 20, 25, 30],
1838 fuelTanks = B767Model.fuelTanks,
1839 numEngines = 2)
1840
1841 @property
1842 def name(self):
1843 """Get the name for this aircraft model."""
1844 return "FSUIPC/Generic Boeing 767"
1845
1846#------------------------------------------------------------------------------
1847
1848class DH8DModel(GenericAircraftModel):
1849 """Generic model for the Bombardier Dash 8-Q400 aircraft."""
1850 fuelTanks = [const.FUELTANK_LEFT, const.FUELTANK_RIGHT]
1851
1852 def __init__(self):
1853 """Construct the model."""
1854 super(DH8DModel, self). \
1855 __init__(flapsNotches = [0, 5, 10, 15, 35],
1856 fuelTanks = DH8DModel.fuelTanks,
1857 numEngines = 2)
1858
1859 @property
1860 def name(self):
1861 """Get the name for this aircraft model."""
1862 return "FSUIPC/Generic Bombardier Dash 8-Q400"
1863
1864#------------------------------------------------------------------------------
1865
1866class DreamwingsDH8DModel(DH8DModel):
1867 """Model handler for the Dreamwings Dash 8-Q400."""
1868 @staticmethod
1869 def doesHandle(aircraft, aircraftName):
1870 """Determine if this model handler handles the aircraft with the given
1871 name."""
1872 (name, airPath) = aircraftName
1873 return aircraft.type==const.AIRCRAFT_DH8D and \
1874 (name.find("Dreamwings")!=-1 or airPath.find("Dreamwings")!=-1) and \
1875 (name.find("Dash")!=-1 or airPath.find("Dash")!=-1) and \
1876 (name.find("Q400")!=-1 or airPath.find("Q400")!=-1) and \
1877 airPath.find("Dash8Q400")!=-1
1878
1879 @property
1880 def name(self):
1881 """Get the name for this aircraft model."""
1882 return "FSUIPC/Dreamwings Bombardier Dash 8-Q400"
1883
1884 def addMonitoringData(self, data, fsType):
1885 """Add the model-specific monitoring data to the given array."""
1886 super(DreamwingsDH8DModel, self).addMonitoringData(data, fsType)
1887
1888 self._addOffsetWithIndexMember(data, 0x132c, "d", "_dwdh8d_navgps")
1889
1890 def getAircraftState(self, aircraft, timestamp, data):
1891 """Get the aircraft state.
1892
1893 Get it from the parent, and then invert the pitot heat state."""
1894 state = super(DreamwingsDH8DModel, self).getAircraftState(aircraft,
1895 timestamp,
1896 data)
1897 if data[self._dwdh8d_navgps]==1:
1898 state.apHeading = None
1899
1900 return state
1901
1902#------------------------------------------------------------------------------
1903
1904class MajesticDH8DModel(DH8DModel):
1905 """Model handler for the Majestic Dash 8-Q400."""
1906 @staticmethod
1907 def doesHandle(aircraft, aircraftName):
1908 """Determine if this model handler handles the aircraft with the given
1909 name."""
1910 (name, airPath) = aircraftName
1911 return aircraft.type==const.AIRCRAFT_DH8D and \
1912 (name.find("MJC8Q400")!=-1 or \
1913 airPath.lower().find("mjc8q400") or \
1914 airPath.lower().find("mjc8q4.air"))
1915
1916 @property
1917 def name(self):
1918 """Get the name for this aircraft model."""
1919 return "FSUIPC/Majestic Bombardier Dash 8-Q400"
1920
1921 def getAircraftState(self, aircraft, timestamp, data):
1922 """Get the aircraft state.
1923
1924 Get it from the parent, and then clear the anti-collision and landing
1925 lights."""
1926 state = super(MajesticDH8DModel, self).getAircraftState(aircraft,
1927 timestamp,
1928 data)
1929 state.antiCollisionLightsOn = None
1930 state.strobeLightsOn = None
1931 state.pitotHeatOn = None
1932
1933 # G-load seems to be offset by -1.0 (i.e a value of 0 seem to mean
1934 # a G-load of 1.0)
1935 state.gLoad += 1.0
1936
1937 # None of the gear values seem to work correctly
1938 state.gearsDown = state.gearControlDown
1939
1940 # Th N1 values cannot be read either
1941 state.n1 = [None, None]
1942
1943 return state
1944
1945#------------------------------------------------------------------------------
1946
1947class CRJ2Model(GenericAircraftModel):
1948 """Generic model for the Bombardier CRJ-200 aircraft."""
1949 fuelTanks = [const.FUELTANK_LEFT, const.FUELTANK_CENTRE, const.FUELTANK_RIGHT]
1950
1951 def __init__(self):
1952 """Construct the model."""
1953 super(CRJ2Model, self). \
1954 __init__(flapsNotches = [0, 8, 20, 30, 45],
1955 fuelTanks = CRJ2Model.fuelTanks,
1956 numEngines = 2)
1957
1958 @property
1959 def name(self):
1960 """Get the name for this aircraft model."""
1961 return "FSUIPC/Generic Bombardier CRJ-200"
1962
1963#------------------------------------------------------------------------------
1964
1965class F70Model(GenericAircraftModel):
1966 """Generic model for the Fokker F70 aircraft."""
1967 fuelTanks = [const.FUELTANK_LEFT, const.FUELTANK_CENTRE, const.FUELTANK_RIGHT]
1968
1969 def __init__(self):
1970 """Construct the model."""
1971 super(F70Model, self). \
1972 __init__(flapsNotches = [0, 8, 15, 25, 42],
1973 fuelTanks = F70Model.fuelTanks,
1974 numEngines = 2)
1975
1976 @property
1977 def name(self):
1978 """Get the name for this aircraft model."""
1979 return "FSUIPC/Generic Fokker 70"
1980
1981#------------------------------------------------------------------------------
1982
1983class DAF70Model(F70Model):
1984 """Model for the Digital Aviation F70 implementation on FS9."""
1985 @staticmethod
1986 def doesHandle(aircraft, aircraftName):
1987 """Determine if this model handler handles the aircraft with the given
1988 name."""
1989 (name, airPath) = aircraftName
1990 return aircraft.type == const.AIRCRAFT_F70 and \
1991 (airPath.endswith("fokker70_2k4_v4.1.air") or
1992 airPath.endswith("fokker70_2k4_v4.3.air") or
1993 airPath.lower().endswith("fokker70_fsx_v4.3.air"))
1994
1995 @property
1996 def name(self):
1997 """Get the name for this aircraft model."""
1998 return "FSUIPC/Digital Aviation Fokker 70"
1999
2000 def getAircraftState(self, aircraft, timestamp, data):
2001 """Get the aircraft state.
2002
2003 Get it from the parent, and then invert the pitot heat state."""
2004 state = super(DAF70Model, self).getAircraftState(aircraft,
2005 timestamp,
2006 data)
2007 state.navLightsOn = None
2008 state.landingLightsOn = None
2009
2010 state.altimeterReliable = False
2011
2012 state.ils = state.nav1
2013 state.ils_obs = state.nav1_obs
2014 state.ils_manual = state.nav1_manual
2015
2016 state.nav1 = state.nav2
2017 state.nav1_obs = state.nav2_obs
2018 state.nav1_manual = aircraft.flight.stage!=const.STAGE_CRUISE
2019
2020 state.nav2 = None
2021 state.nav2_obs = None
2022 state.nav2_manual = False
2023
2024 state.autoXPDR = True
2025
2026 return state
2027
2028#------------------------------------------------------------------------------
2029
2030class DC3Model(GenericAircraftModel):
2031 """Generic model for the Lisunov Li-2 (DC-3) aircraft."""
2032 fuelTanks = [const.FUELTANK_LEFT, const.FUELTANK_CENTRE,
2033 const.FUELTANK_RIGHT]
2034 # fuelTanks = [const.FUELTANK_LEFT_AUX, const.FUELTANK_LEFT,
2035 # const.FUELTANK_RIGHT, const.FUELTANK_RIGHT_AUX]
2036
2037 def __init__(self):
2038 """Construct the model."""
2039 super(DC3Model, self). \
2040 __init__(flapsNotches = [0, 15, 30, 45],
2041 fuelTanks = DC3Model.fuelTanks,
2042 numEngines = 2, isN1 = False)
2043 self._leftLevel = 0.0
2044 self._rightLevel = 0.0
2045
2046 @property
2047 def name(self):
2048 """Get the name for this aircraft model."""
2049 return "FSUIPC/Generic Lisunov Li-2 (DC-3)"
2050
2051 def _convertFuelData(self, data, index = 0, addCapacities = False):
2052 """Convert the given data into a fuel info list.
2053
2054 It assumes to receive the 3 fuel tanks as seen above (left,
2055 centre and right) and converts it to left aux, left, right,
2056 and right aux. The amount in the left tank goes into left aux,
2057 the amount of the right tank goes into right aux and the
2058 amount of the centre tank goes into the left and right tanks
2059 evenly distributed."""
2060 (rawFuelData, totalFuel) = \
2061 super(DC3Model, self)._convertFuelData(data, index, addCapacities)
2062
2063 centreAmount = rawFuelData[1][1]
2064 if addCapacities:
2065 centreCapacity = rawFuelData[1][2]
2066 self._leftLevel = self._rightLevel = \
2067 centreAmount / centreCapacity / 2.0
2068 fuelData = [(const.FUELTANK_LEFT_AUX,
2069 rawFuelData[0][1], rawFuelData[0][2]),
2070 (const.FUELTANK_LEFT,
2071 centreAmount/2.0, centreCapacity/2.0),
2072 (const.FUELTANK_RIGHT,
2073 centreAmount/2.0, centreCapacity/2.0),
2074 (const.FUELTANK_RIGHT_AUX,
2075 rawFuelData[2][1], rawFuelData[2][2])]
2076 else:
2077 fuelData = [(const.FUELTANK_LEFT_AUX, rawFuelData[0][1]),
2078 (const.FUELTANK_LEFT, centreAmount/2.0),
2079 (const.FUELTANK_RIGHT, centreAmount/2.0),
2080 (const.FUELTANK_RIGHT_AUX, rawFuelData[2][1])]
2081
2082 return (fuelData, totalFuel)
2083
2084 def setFuelLevel(self, handler, levels):
2085 """Set the fuel level.
2086
2087 See the description of Simulator.setFuelLevel. This
2088 implementation assumes to get the four-tank representation,
2089 as returned by getFuel()."""
2090 leftLevel = None
2091 centreLevel = None
2092 rightLevel = None
2093
2094 for (tank, level) in levels:
2095 if tank==const.FUELTANK_LEFT_AUX:
2096 leftLevel = level if leftLevel is None else (leftLevel + level)
2097 elif tank==const.FUELTANK_LEFT:
2098 level /= 2.0
2099 centreLevel = (self._rightLevel + level) \
2100 if centreLevel is None else (centreLevel + level)
2101 self._leftLevel = level
2102 elif tank==const.FUELTANK_RIGHT:
2103 level /= 2.0
2104 centreLevel = (self._leftLevel + level) \
2105 if centreLevel is None else (centreLevel + level)
2106 self._rightLevel = level
2107 elif tank==const.FUELTANK_RIGHT_AUX:
2108 rightLevel = level if rightLevel is None \
2109 else (rightLevel + level)
2110
2111 levels = []
2112 if leftLevel is not None: levels.append((const.FUELTANK_LEFT,
2113 leftLevel))
2114 if centreLevel is not None: levels.append((const.FUELTANK_CENTRE,
2115 centreLevel))
2116 if rightLevel is not None: levels.append((const.FUELTANK_RIGHT,
2117 rightLevel))
2118
2119 super(DC3Model, self).setFuelLevel(handler, levels)
2120
2121#------------------------------------------------------------------------------
2122
2123class T134Model(GenericAircraftModel):
2124 """Generic model for the Tupolev Tu-134 aircraft."""
2125 fuelTanks = [const.FUELTANK_LEFT_TIP, const.FUELTANK_EXTERNAL1,
2126 const.FUELTANK_LEFT_AUX,
2127 const.FUELTANK_CENTRE,
2128 const.FUELTANK_RIGHT_AUX,
2129 const.FUELTANK_EXTERNAL2, const.FUELTANK_RIGHT_TIP]
2130
2131 def __init__(self):
2132 """Construct the model."""
2133 super(T134Model, self). \
2134 __init__(flapsNotches = [0, 10, 20, 30],
2135 fuelTanks = T134Model.fuelTanks,
2136 numEngines = 2)
2137
2138 @property
2139 def name(self):
2140 """Get the name for this aircraft model."""
2141 return "FSUIPC/Generic Tupolev Tu-134"
2142
2143#------------------------------------------------------------------------------
2144
2145class T154Model(GenericAircraftModel):
2146 """Generic model for the Tupolev Tu-134 aircraft."""
2147 fuelTanks = [const.FUELTANK_LEFT_AUX, const.FUELTANK_LEFT,
2148 const.FUELTANK_CENTRE, const.FUELTANK_CENTRE2,
2149 const.FUELTANK_RIGHT, const.FUELTANK_RIGHT_AUX]
2150
2151 def __init__(self):
2152 """Construct the model."""
2153 super(T154Model, self). \
2154 __init__(flapsNotches = [0, 15, 28, 45],
2155 fuelTanks = T154Model.fuelTanks,
2156 numEngines = 3)
2157
2158 @property
2159 def name(self):
2160 """Get the name for this aircraft model."""
2161 return "FSUIPC/Generic Tupolev Tu-154"
2162
2163 def getAircraftState(self, aircraft, timestamp, data):
2164 """Get an aircraft state object for the given monitoring data.
2165
2166 This removes the reverser value for the middle engine."""
2167 state = super(T154Model, self).getAircraftState(aircraft, timestamp, data)
2168 del state.reverser[1]
2169 return state
2170
2171#------------------------------------------------------------------------------
2172
2173class PTT154Model(T154Model):
2174 """Project Tupolev Tu-154."""
2175 @staticmethod
2176 def doesHandle(aircraft, aircraftName):
2177 """Determine if this model handler handles the aircraft with the given
2178 name."""
2179 (name, airPath) = aircraftName
2180 print("PTT154Model.doesHandle", aircraft.type, name, airPath)
2181 return aircraft.type==const.AIRCRAFT_T154 and \
2182 (name.find("Tu-154")!=-1 or name.find("Tu154B")!=-1) and \
2183 os.path.basename(airPath).startswith("154b_")
2184
2185 def __init__(self):
2186 """Construct the model."""
2187 super(PTT154Model, self).__init__()
2188
2189 @property
2190 def name(self):
2191 """Get the name for this aircraft model."""
2192 return "FSUIPC/Project Tupolev Tu-154"
2193
2194 def addMonitoringData(self, data, fsType):
2195 """Add the model-specific monitoring data to the given array.
2196
2197 It only stores the flight simulator type."""
2198 super(PTT154Model, self).addMonitoringData(data, fsType)
2199
2200 def getAircraftState(self, aircraft, timestamp, data):
2201 """Get an aircraft state object for the given monitoring data.
2202
2203 This removes the reverser value for the middle engine."""
2204 state = super(PTT154Model, self).getAircraftState(aircraft, timestamp, data)
2205
2206 if self._fsType==const.SIM_MSFSX or self._fsType==const.SIM_P3D:
2207 state.xpdrC = None
2208
2209 return state
2210
2211
2212#------------------------------------------------------------------------------
2213
2214class YK40Model(GenericAircraftModel):
2215 """Generic model for the Yakovlev Yak-40 aircraft."""
2216 fuelTanks = [const.FUELTANK_LEFT, const.FUELTANK_RIGHT]
2217
2218 def __init__(self):
2219 """Construct the model."""
2220 super(YK40Model, self). \
2221 __init__(flapsNotches = [0, 20, 35],
2222 fuelTanks = YK40Model.fuelTanks,
2223 numEngines = 2)
2224
2225 @property
2226 def name(self):
2227 """Get the name for this aircraft model."""
2228 return "FSUIPC/Generic Yakovlev Yak-40"
2229
2230#------------------------------------------------------------------------------
2231
2232class B462Model(GenericAircraftModel):
2233 """Generic model for the British Aerospace BAe 146-200 aircraft."""
2234 fuelTanks = [const.FUELTANK_LEFT, const.FUELTANK_CENTRE,
2235 const.FUELTANK_RIGHT]
2236
2237 def __init__(self):
2238 """Construct the model."""
2239 super(B462Model, self). \
2240 __init__(flapsNotches = [0, 18, 24, 30, 33],
2241 fuelTanks = B462Model.fuelTanks,
2242 numEngines = 4)
2243
2244 @property
2245 def name(self):
2246 """Get the name for this aircraft model."""
2247 return "FSUIPC/Generic British Aerospace 146"
2248
2249 def getAircraftState(self, aircraft, timestamp, data):
2250 """Get an aircraft state object for the given monitoring data.
2251
2252 This removes the reverser value for the middle engine."""
2253 state = super(B462Model, self).getAircraftState(aircraft, timestamp, data)
2254 state.reverser = []
2255 return state
2256
2257#------------------------------------------------------------------------------
2258
2259_genericModels = { const.AIRCRAFT_B736 : B737Model,
2260 const.AIRCRAFT_B737 : B737Model,
2261 const.AIRCRAFT_B738 : B737Model,
2262 const.AIRCRAFT_B738C : B737Model,
2263 const.AIRCRAFT_B732 : B737Model,
2264 const.AIRCRAFT_B733 : B737Model,
2265 const.AIRCRAFT_B734 : B737Model,
2266 const.AIRCRAFT_B735 : B737Model,
2267 const.AIRCRAFT_DH8D : DH8DModel,
2268 const.AIRCRAFT_B762 : B767Model,
2269 const.AIRCRAFT_B763 : B767Model,
2270 const.AIRCRAFT_CRJ2 : CRJ2Model,
2271 const.AIRCRAFT_F70 : F70Model,
2272 const.AIRCRAFT_DC3 : DC3Model,
2273 const.AIRCRAFT_T134 : T134Model,
2274 const.AIRCRAFT_T154 : T154Model,
2275 const.AIRCRAFT_YK40 : YK40Model,
2276 const.AIRCRAFT_B462 : B462Model }
2277
2278#------------------------------------------------------------------------------
2279
2280AircraftModel.registerSpecial(PMDGBoeing737NGModel)
2281AircraftModel.registerSpecial(DreamwingsDH8DModel)
2282AircraftModel.registerSpecial(MajesticDH8DModel)
2283AircraftModel.registerSpecial(DAF70Model)
2284AircraftModel.registerSpecial(PTT154Model)
2285
2286#------------------------------------------------------------------------------
Note: See TracBrowser for help on using the repository browser.