source: src/mlx/fsuipc.py@ 1086:54924f6e0818

python3
Last change on this file since 1086:54924f6e0818 was 1078:a1e7d4f63210, checked in by István Váradi <ivaradi@…>, 22 months ago

PMDG on MSFS 2020 support (re #364)

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