source: src/mlx/fsuipc.py@ 919:2ce8ca39525b

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

Ran 2to3

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