source: src/mlx/fsuipc.py@ 1019:d45b3d2eeb1d

python3
Last change on this file since 1019:d45b3d2eeb1d was 1015:a6e03fae41e0, checked in by István Váradi <ivaradi@…>, 5 years ago

Adapted to the new interface of PyUIPC using bytes for strings (re #347)

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