source: src/mlx/fsuipc.py@ 1045:110ed385e042

python3
Last change on this file since 1045:110ed385e042 was 1022:b96b66bd7213, checked in by István Váradi <ivaradi@…>, 4 years ago

A periodic request is not reinserted into the one-shot request queue on a connection failure (re #349)

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