source: src/mlx/fsuipc.py@ 960:8e3a82e27332

python3
Last change on this file since 960:8e3a82e27332 was 960:8e3a82e27332, checked in by István Váradi <ivaradi@…>, 5 years ago

Removed unnecessary latin-1 decoder from the FSUIPC handler (re #347)

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