source: src/mlx/xplane.py@ 1103:03fdfc6c9577

python3
Last change on this file since 1103:03fdfc6c9577 was 1103:03fdfc6c9577, checked in by István Váradi <ivaradi@…>, 8 months ago

Timestamp monotonicity on X-Plane is checked only while monitoring

File size: 91.8 KB
Line 
1
2from . import fs
3from . import const
4from . import util
5from .watchdog import Watchdog
6
7import threading
8import time
9import calendar
10import datetime
11import sys
12import codecs
13import math
14from functools import total_ordering
15
16from xplra import XPlane, MultiGetter, MultiSetter, ProtocolException
17from xplra import TYPE_INT, TYPE_FLOAT, TYPE_DOUBLE
18from xplra import TYPE_FLOAT_ARRAY, TYPE_INT_ARRAY, TYPE_BYTE_ARRAY
19from xplra import HOTKEY_MODIFIER_SHIFT, HOTKEY_MODIFIER_CONTROL
20
21#------------------------------------------------------------------------------
22
23## @package mlx.xplane
24#
25# The module towards X-Plane
26#
27# This module implements the simulator interface to X-Plane via the
28# X-Plane Remote Access (xplra) plugin.
29
30#------------------------------------------------------------------------------
31
32_hgin2hpa = 1013.25 / 29.92
33
34_mps2knots = 3600.0 / 1852
35
36#------------------------------------------------------------------------------
37
38class Request(object):
39 """Base class for one-shot requests."""
40 def __init__(self, handler, callback, extra):
41 """Construct the request."""
42 self._handler = handler
43 self._callback = callback
44 self._extra = extra
45 self._result = None
46
47 def process(self, time):
48 """Process the request.
49
50 Return True if the request has succeeded, False if data validation
51 has failed for a reading request. An exception may also be thrown
52 if there is some lower-level communication problem."""
53 if self._process(time):
54 Handler._callSafe(lambda: self._callback(self._result,
55 self._extra))
56 return True
57 else:
58 return False
59
60 def fail(self):
61 """Handle the failure of this request."""
62 Handler._callSafe(lambda: self._callback(False, self._extra))
63
64class DataRequest(Request):
65 """A simple, one-shot data read or write request."""
66 def __init__(self, handler, forWrite, data, callback, extra,
67 validator = None):
68 """Construct the request."""
69 super(DataRequest, self).__init__(handler, callback, extra)
70
71 self._forWrite = forWrite
72 self._validator = validator
73
74 xplane = handler._xplane
75 self._multiBuffer = xplane.createMultiSetter() if forWrite \
76 else xplane.createMultiGetter()
77
78 Handler._setupMultiBuffer(self._multiBuffer,
79 [(d[0], d[1]) for d in data])
80
81 if forWrite:
82 index = 0
83 for (_, _, value) in data:
84 self._multiBuffer[index] = value
85 index += 1
86
87
88 def fail(self):
89 """Handle the failure of this request."""
90 if self._forWrite:
91 super(DataRequest, self).fail()
92 else:
93 Handler._callSafe(lambda: self._callback(None, self._extra))
94
95 def _process(self, time):
96 """Process the request."""
97 if self._forWrite:
98 self._multiBuffer.execute()
99 self._result = True
100 return True
101
102 try:
103 if Handler._performRead(self._multiBuffer,
104 self._extra, self._validator):
105 self._result = self._multiBuffer
106 return True
107 else:
108 return False
109 except ProtocolException as e:
110 self._result = None
111 return True
112
113class ShowMessageRequest(Request):
114 """Request to show a message in the simulator window."""
115 def __init__(self, handler, message, duration, callback, extra):
116 """Construct the request."""
117 super(ShowMessageRequest, self).__init__(handler,
118 callback, extra)
119 self._message = message
120 self._duration = duration
121
122 def _process(self, time):
123 """Process the request."""
124 self._handler._xplane.showMessage(self._message, self._duration)
125 self._result = True
126 return True
127
128class RegisterHotkeysRequest(Request):
129 """Request to register hotkeys with the simulator."""
130 def __init__(self, handler, hotkeyCodes, callback, extra):
131 """Construct the request."""
132 super(RegisterHotkeysRequest, self).__init__(handler,
133 callback,
134 extra)
135 self._hotkeyCodes = hotkeyCodes
136
137 def _process(self, time):
138 """Process the request."""
139 self._handler._xplane.registerHotkeys(self._hotkeyCodes)
140 self._result = True
141 return True
142
143class UnregisterHotkeysRequest(Request):
144 """Request to register hotkeys with the simulator."""
145 def _process(self, time):
146 """Process the request."""
147 self._handler._xplane.unregisterHotkeys()
148 self._result = True
149 return True
150
151@total_ordering
152class PeriodicRequest(object):
153 """A periodic request."""
154 def __init__(self, handler, id, period, callback, extra):
155 """Construct the periodic request."""
156 self._handler = handler
157 self._id = id
158 self._period = period
159 self._nextFire = time.time()
160 self._callback = callback
161 self._extra = extra
162 self._result = None
163
164 @property
165 def id(self):
166 """Get the ID of this periodic request."""
167 return self._id
168
169 @property
170 def nextFire(self):
171 """Get the next firing time."""
172 return self._nextFire
173
174 def process(self, now):
175 """Check if this request should be executed, and if so, do so.
176
177 now is the time at which the request is being executed. If this
178 function is called too early, nothing is done, and True is
179 returned.
180
181 Return True if the request has succeeded, False if data validation
182 has failed. An exception may also be thrown if there is some
183 lower-level communication problem."""
184 if now<self._nextFire:
185 return True
186
187 isOK = self._process(time)
188
189 if isOK:
190 Handler._callSafe(lambda: self._callback(self._result,
191 self._extra))
192 now = time.time()
193 while self._nextFire <= now:
194 self._nextFire += self._period
195
196 return isOK
197
198 def fail(self):
199 """Handle the failure of this request."""
200 pass
201
202 def __eq__(self, other):
203 """Equality comparison by the firing times"""
204 return self._nextFire == other._nextFire
205
206 def __ne__(self, other):
207 """Non-equality comparison by the firing times"""
208 return self._nextFire != other._nextFire
209
210 def __lt__(self, other):
211 """Less-than comparison by the firing times"""
212 return self._nextFire < other._nextFire
213
214class PeriodicDataRequest(PeriodicRequest):
215 """A periodic request."""
216 def __init__(self, handler, id, period, data, callback, extra,
217 validator):
218 """Construct the periodic request."""
219 super(PeriodicDataRequest, self).__init__(handler, id, period,
220 callback, extra)
221 self._validator = validator
222 self._multiGetter = handler._xplane.createMultiGetter()
223 Handler._setupMultiBuffer(self._multiGetter, data)
224
225 def _process(self, now):
226 """Process the request."""
227 if Handler._performRead(self._multiGetter,
228 self._extra, self._validator):
229 self._result = self._multiGetter
230 return True
231 else:
232 return False
233
234#------------------------------------------------------------------------------
235
236class HotkeysStateRequest(PeriodicRequest):
237 """Periodic hotkey query request."""
238 def _process(self, now):
239 """Process the request."""
240 self._result = self._handler._xplane.queryHotkeys()
241 return True
242
243#------------------------------------------------------------------------------
244
245class Handler(threading.Thread):
246 """The thread to handle the requests towards X-Plane."""
247 @staticmethod
248 def _callSafe(fun):
249 """Call the given function and swallow any exceptions."""
250 try:
251 return fun()
252 except Exception as e:
253 print(util.utf2unicode(str(e)), file=sys.stderr)
254 return None
255
256 # The number of times a read is attempted
257 NUM_READATTEMPTS = 3
258
259 # The number of connection attempts
260 NUM_CONNECTATTEMPTS = 3
261
262 # The interval between successive connect attempts
263 CONNECT_INTERVAL = 0.25
264
265 @staticmethod
266 def _setupMultiBuffer(buffer, dataSpec):
267 """Setup the given multi-dataref buffer for the given data
268 specification.
269
270 The specification is a list of tuples of two items:
271 - the name of the dataref
272 - the type of the dataref. It can be one of the following:
273 - an integer denoting the type. If it denotes an array type, the
274 length will be -1 (i.e. as many as returned when reading the
275 value), and the offset will be 0
276 - a tuple of two or three items:
277 - the first item is the type constant
278 - the second item is the length
279 - the third item is the offset, which defaults to 0."""
280 for (name, typeInfo) in dataSpec:
281 length = -1
282 offset = 0
283 type = 0
284 if isinstance(typeInfo, tuple):
285 type = typeInfo[0]
286 length = typeInfo[1]
287 offset = 0 if len(typeInfo)<3 else typeInfo[2]
288 else:
289 type = typeInfo
290
291 if type==TYPE_INT:
292 buffer.addInt(name)
293 elif type==TYPE_FLOAT:
294 buffer.addFloat(name)
295 elif type==TYPE_DOUBLE:
296 buffer.addDouble(name)
297 elif type==TYPE_FLOAT_ARRAY:
298 buffer.addFloatArray(name, length = length, offset = offset)
299 elif type==TYPE_INT_ARRAY:
300 buffer.addIntArray(name, length = length, offset = offset)
301 elif type==TYPE_BYTE_ARRAY:
302 buffer.addByteArray(name, length = length, offset = offset)
303 else:
304 raise TypeError("xplane.Handler._setupMultiBuffer: invalid type info: %s for dataref '%s'" % (typeInfo, name))
305
306
307
308 @staticmethod
309 def _performRead(multiGetter, extra, validator):
310 """Perform a read request.
311
312 If there is a validator, that will be called with the return values,
313 and if the values are wrong, the request is retried at most a certain
314 number of times.
315
316 Return True if the request has succeeded, False if validation has
317 failed during all attempts. An exception may also be thrown if there is
318 some lower-level communication problem."""
319 attemptsLeft = Handler.NUM_READATTEMPTS
320 while attemptsLeft>0:
321 try:
322 multiGetter.execute()
323 except ProtocolException as e:
324 print("xplane.Handler._performRead: " + str(e))
325 raise
326
327 if validator is None or \
328 Handler._callSafe(lambda: validator(multiGetter, extra)):
329 return True
330 else:
331 attemptsLeft -= 1
332 return False
333
334 def __init__(self, connectionListener,
335 connectAttempts = -1, connectInterval = 0.2):
336 """Construct the handler with the given connection listener."""
337 threading.Thread.__init__(self)
338
339 self._connectionListener = connectionListener
340 self._connectAttempts = connectAttempts
341 self._connectInterval = connectInterval
342
343 self._xplane = XPlane()
344
345 self._requestCondition = threading.Condition()
346 self._connectionRequested = False
347 self._connected = False
348
349 self._requests = []
350 self._nextPeriodicID = 1
351 self._periodicRequests = []
352
353 self._watchdogClient = Watchdog.get().addClient(2.0, "xplane.Handler")
354
355 self.daemon = True
356
357 def requestRead(self, data, callback, extra = None, validator = None):
358 """Request the reading of some data.
359
360 data is a list of tuples of the following items:
361 - the offset of the data as an integer
362 - the type letter of the data as a string
363
364 callback is a function that receives two pieces of data:
365 - the values retrieved or None on error
366 - the extra parameter
367
368 It will be called in the handler's thread!
369 """
370 with self._requestCondition:
371 self._requests.append(DataRequest(self, False, data,
372 callback, extra,
373 validator))
374 self._requestCondition.notify()
375
376 def requestWrite(self, data, callback, extra = None):
377 """Request the writing of some data.
378
379 data is a list of tuples of the following items:
380 - the offset of the data as an integer
381 - the type letter of the data as a string
382 - the data to write
383
384 callback is a function that receives two pieces of data:
385 - a boolean indicating if writing was successful
386 - the extra data
387 It will be called in the handler's thread!
388 """
389 with self._requestCondition:
390 request = DataRequest(self, True, data, callback, extra)
391 #print "xplane.Handler.requestWrite", request
392 self._requests.append(request)
393 self._requestCondition.notify()
394
395 def requestPeriodicRead(self, period, data, callback, extra = None,
396 validator = None):
397 """Request a periodic read of data.
398
399 period is a floating point number with the period in seconds.
400
401 This function returns an identifier which can be used to cancel the
402 request."""
403 with self._requestCondition:
404 id = self._nextPeriodicID
405 self._nextPeriodicID += 1
406 request = PeriodicDataRequest(self, id, period,
407 data, callback,
408 extra, validator)
409 self._periodicRequests.append(request)
410 self._requestCondition.notify()
411 return id
412
413 def clearPeriodic(self, id):
414 """Clear the periodic request with the given ID."""
415 with self._requestCondition:
416 for i in range(0, len(self._periodicRequests)):
417 if self._periodicRequests[i].id==id:
418 del self._periodicRequests[i]
419 return True
420 return False
421
422 def requestShowMessage(self, message, duration, callback, extra = None):
423 """Request showing a message in the simulator."""
424 with self._requestCondition:
425 self._requests.append(ShowMessageRequest(self,
426 message, duration,
427 callback, extra))
428 self._requestCondition.notify()
429
430 def registerHotkeys(self, hotkeys, callback, extra = None):
431 """Request registering the given hotkeys."""
432 with self._requestCondition:
433 self._requests.append(RegisterHotkeysRequest(self, hotkeys,
434 callback, extra))
435 self._requestCondition.notify()
436
437 def requestHotkeysState(self, period, callback, extra = None):
438 """Request a periodic query of the hotkey status."""
439 with self._requestCondition:
440 id = self._nextPeriodicID
441 self._nextPeriodicID += 1
442 request = HotkeysStateRequest(self, id, period, callback, extra)
443 self._periodicRequests.append(request)
444 self._requestCondition.notify()
445 return id
446
447 def unregisterHotkeys(self, callback, extra = None):
448 """Request unregistering the hotkeys."""
449 with self._requestCondition:
450 self._requests.append(UnregisterHotkeysRequest(self,
451 callback, extra))
452 self._requestCondition.notify()
453
454 def connect(self):
455 """Initiate the connection to the flight simulator."""
456 with self._requestCondition:
457 if not self._connectionRequested:
458 self._connectionRequested = True
459 self._requestCondition.notify()
460
461 def disconnect(self):
462 """Disconnect from the flight simulator."""
463 with self._requestCondition:
464 self._requests = []
465 if self._connectionRequested:
466 self._connectionRequested = False
467 self._requestCondition.notify()
468
469 def clearRequests(self):
470 """Clear the outstanding one-shot requests."""
471 with self._requestCondition:
472 self._requests = []
473
474 def run(self):
475 """Perform the operation of the thread."""
476 while True:
477 self._waitConnectionRequest()
478
479 if self._connect()>0:
480 self._handleConnection()
481
482 self._disconnect()
483
484 def _waitConnectionRequest(self):
485 """Wait for a connection request to arrive."""
486 with self._requestCondition:
487 while not self._connectionRequested:
488 self._requestCondition.wait()
489
490 def _connect(self, autoReconnection = False, attempts = 0):
491 """Try to connect to the flight simulator via XPLRA
492
493 Returns True if the connection has been established, False if it was
494 not due to no longer requested.
495 """
496 config = self._connectionListener.config
497 if config.xplaneRemote:
498 address = "tcp:" + config.xplaneAddress
499 else:
500 address = "local"
501
502 print("xplane.Handler._connect: address:", address)
503
504 while self._connectionRequested:
505 if attempts>=self.NUM_CONNECTATTEMPTS:
506 self._connectionRequested = False
507 if autoReconnection:
508 Handler._callSafe(lambda:
509 self._connectionListener.disconnected())
510 else:
511 Handler._callSafe(lambda:
512 self._connectionListener.connectionFailed())
513 return 0
514
515 try:
516 attempts += 1
517 self._xplane.connect(address = address)
518
519 (xplaneVersion, xplmVersion, xplraVersion) = \
520 self._xplane.getVersions()
521
522 description = "(X-Plane version: %d, XPLM version: %d, XPLRA version: %03d)" % \
523 (xplaneVersion, xplmVersion, xplraVersion)
524 if not autoReconnection:
525 fsType = \
526 const.SIM_XPLANE12 if xplaneVersion>=12000 else \
527 const.SIM_XPLANE11 if xplaneVersion>=11000 else \
528 const.SIM_XPLANE10 if xplaneVersion>=10000 else \
529 const.SIM_XPLANE9
530
531 print("xplane.Handler._connect: fsType:", fsType)
532
533 Handler._callSafe(lambda:
534 self._connectionListener.connected(fsType,
535 description))
536 self._connected = True
537 return attempts
538 except Exception as e:
539 print("xplane.Handler._connect: connection failed: " + \
540 util.utf2unicode(str(e)) + \
541 " (attempts: %d)" % (attempts,))
542 if attempts<self.NUM_CONNECTATTEMPTS:
543 time.sleep(self.CONNECT_INTERVAL)
544 self._xplane.disconnect()
545
546 def _handleConnection(self):
547 """Handle a living connection."""
548 with self._requestCondition:
549 while self._connectionRequested:
550 self._processRequests()
551 self._waitRequest()
552
553 def _waitRequest(self):
554 """Wait for the time of the next request.
555
556 Returns also, if the connection is no longer requested.
557
558 Should be called with the request condition lock held."""
559 while self._connectionRequested:
560 timeout = None
561 if self._periodicRequests:
562 self._periodicRequests.sort()
563 timeout = self._periodicRequests[0].nextFire - time.time()
564
565 if self._requests or \
566 (timeout is not None and timeout <= 0.0):
567 return
568
569 self._requestCondition.wait(timeout)
570
571 def _disconnect(self):
572 """Disconnect from the flight simulator."""
573 print("xplane.Handler._disconnect")
574 if self._connected:
575 try:
576 self._xplane.disconnect()
577 except:
578 pass
579
580 self._connected = False
581
582 def _processRequest(self, request, time, attempts, isPeriodic):
583 """Process the given request.
584
585 If an exception occurs or invalid data is read too many times, we try
586 to reconnect.
587
588 This function returns only if the request has succeeded, or if a
589 connection is no longer requested.
590
591 This function is called with the request lock held, but is relased
592 whole processing the request and reconnecting."""
593 self._requestCondition.release()
594
595 #print "xplane.Handler._processRequest", request
596
597 needReconnect = False
598 try:
599 self._watchdogClient.set()
600 try:
601 if not request.process(time):
602 print("xplane.Handler._processRequest: X-Plane returned invalid data too many times, reconnecting")
603 needReconnect = True
604 except Exception as e:
605 print("xplane.Handler._processRequest: X-Plane connection failed (" + \
606 util.utf2unicode(str(e)) + \
607 "), reconnecting (attempts=%d)." % (attempts,))
608 needReconnect = True
609
610 if needReconnect:
611 with self._requestCondition:
612 if not isPeriodic:
613 self._requests.insert(0, request)
614 self._disconnect()
615 return self._connect(autoReconnection = True, attempts = attempts)
616 else:
617 return 0
618 finally:
619 self._watchdogClient.clear()
620 self._requestCondition.acquire()
621
622 def _processRequests(self):
623 """Process any pending requests.
624
625 Will be called with the request lock held."""
626 attempts = 0
627 while self._connectionRequested and self._periodicRequests:
628 self._periodicRequests.sort()
629 request = self._periodicRequests[0]
630
631 t = time.time()
632
633 if request.nextFire>t:
634 break
635
636 attempts = self._processRequest(request, t, attempts, True)
637
638 while self._connectionRequested and self._requests:
639 request = self._requests[0]
640 del self._requests[0]
641
642 attempts = self._processRequest(request, None, attempts, False)
643
644 return self._connectionRequested
645
646#------------------------------------------------------------------------------
647
648class Simulator(object):
649 """The simulator class representing the interface to the flight simulator
650 via XPLRA."""
651 # The basic data that should be queried all the time once we are connected
652 timeData = [ ("sim/time/local_date_days", TYPE_INT),
653 ("sim/time/zulu_time_sec", TYPE_FLOAT) ]
654
655 normalData = timeData + \
656 [ ("sim/aircraft/view/acf_tailnum", TYPE_BYTE_ARRAY),
657 ("sim/aircraft/view/acf_author", TYPE_BYTE_ARRAY),
658 ("sim/aircraft/view/acf_descrip", TYPE_BYTE_ARRAY),
659 ("sim/aircraft/view/acf_notes", TYPE_BYTE_ARRAY),
660 ("sim/aircraft/view/acf_ICAO", TYPE_BYTE_ARRAY),
661 ("sim/aircraft/view/acf_livery_path", TYPE_BYTE_ARRAY) ]
662
663 flareData1 = [ ("sim/time/zulu_time_sec", TYPE_FLOAT),
664 ("sim/flightmodel/position/y_agl", TYPE_FLOAT),
665 ("sim/flightmodel/position/vh_ind_fpm2", TYPE_FLOAT) ]
666
667 flareStartData = [ ("sim/weather/wind_speed_kt[0]", TYPE_FLOAT),
668 ("sim/weather/wind_direction_degt[0]", TYPE_FLOAT),
669 ("sim/weather/visibility_reported_m", TYPE_FLOAT) ]
670
671 flareData2 = [ ("sim/time/zulu_time_sec", TYPE_FLOAT),
672 ("sim/flightmodel/failures/onground_any", TYPE_INT),
673 ("sim/flightmodel/position/vh_ind_fpm2", TYPE_FLOAT),
674 ("sim/flightmodel/position/indicated_airspeed2",
675 TYPE_FLOAT),
676 ("sim/flightmodel/position/theta", TYPE_FLOAT),
677 ("sim/flightmodel/position/phi", TYPE_FLOAT),
678 ("sim/flightmodel/position/psi", TYPE_FLOAT) ]
679
680 TIME_SYNC_INTERVAL = 3.0
681
682 @staticmethod
683 def _getHotkeyCode(hotkey):
684 """Get the hotkey code for the given hot key."""
685 code = ord(hotkey.key)
686 if hotkey.shift: code |= HOTKEY_MODIFIER_SHIFT
687 if hotkey.ctrl: code |= HOTKEY_MODIFIER_CONTROL
688 return code
689
690 def __init__(self, connectionListener, connectAttempts = -1,
691 connectInterval = 0.2):
692 """Construct the simulator.
693
694 The aircraft object passed must provide the following members:
695 - type: one of the AIRCRAFT_XXX constants from const.py
696 - modelChanged(aircraftName, modelName): called when the model handling
697 the aircraft has changed.
698 - handleState(aircraftState): handle the given state.
699 - flareStarted(windSpeed, windDirection, visibility, flareStart,
700 flareStartFS): called when the flare has
701 started. windSpeed is in knots, windDirection is in degrees and
702 visibility is in metres. flareStart and flareStartFS are two time
703 values expressed in seconds that can be used to calculate the flare
704 time.
705 - flareFinished(flareEnd, flareEndFS, tdRate, tdRateCalculatedByFS,
706 ias, pitch, bank, heading): called when the flare has
707 finished, i.e. the aircraft is on the ground. flareEnd and flareEndFS
708 are the two time values corresponding to the touchdown time. tdRate is
709 the touch-down rate, tdRateCalculatedBySim indicates if the data comes
710 from the simulator or was calculated by the adapter. The other data
711 are self-explanatory and expressed in their 'natural' units."""
712 self._fsType = None
713 self._aircraft = None
714
715 self._handler = Handler(self,
716 connectAttempts = connectAttempts,
717 connectInterval = connectInterval)
718 self._connectionListener = connectionListener
719 self._handler.start()
720
721 self._syncTime = False
722 self._nextSyncTime = -1
723
724 self._timestampBase = None
725 self._timestampDaysOffset = 0
726 self._lastZuluSeconds = None
727
728 self._normalRequestID = None
729
730 self._monitoringRequested = False
731 self._monitoring = False
732
733 self._aircraftInfo = None
734 self._aircraftModel = None
735
736 self._flareRequestID = None
737 self._flareRates = []
738 self._flareStart = None
739 self._flareStartFS = None
740
741 self._hotkeyLock = threading.Lock()
742 self._hotkeyCodes = None
743 self._hotkeySetID = 0
744 self._hotkeySetGeneration = 0
745 self._hotkeyOffets = None
746 self._hotkeyRequestID = None
747 self._hotkeyCallback = None
748
749 self._fuelCallback = None
750
751 @property
752 def config(self):
753 """Get the configuration."""
754 return self._connectionListener.config
755
756 def connect(self, aircraft):
757 """Initiate a connection to the simulator."""
758 self._aircraft = aircraft
759 self._aircraftInfo = None
760 self._aircraftModel = None
761 self._handler.connect()
762 if self._normalRequestID is None:
763 self._nextSyncTime = -1
764 self._startDefaultNormal()
765
766 def reconnect(self):
767 """Initiate a reconnection to the simulator.
768
769 It does not reset already set up data, just calls connect() on the
770 handler."""
771 self._handler.connect()
772
773 def requestZFW(self, callback):
774 """Send a request for the ZFW."""
775 data = [ ("sim/aircraft/weight/acf_m_empty", TYPE_FLOAT),
776 ("sim/flightmodel/weight/m_fixed", TYPE_FLOAT) ]
777 self._handler.requestRead(data, self._handleZFW, extra = callback)
778
779 def requestWeights(self, callback):
780 """Request the following weights: DOW, ZFW, payload.
781
782 These values will be passed to the callback function in this order, as
783 separate arguments."""
784 data = [ ("sim/aircraft/weight/acf_m_empty", TYPE_FLOAT),
785 ("sim/flightmodel/weight/m_fixed", TYPE_FLOAT),
786 ("sim/flightmodel/weight/m_total", TYPE_FLOAT) ]
787 self._handler.requestRead(data, self._handleWeights,
788 extra = callback)
789
790 def requestTime(self, callback):
791 """Request the time from the simulator."""
792 self._handler.requestRead(Simulator.timeData, self._handleTime,
793 extra = callback)
794
795 def startMonitoring(self):
796 """Start the periodic monitoring of the aircraft and pass the resulting
797 state to the aircraft object periodically."""
798 assert not self._monitoringRequested
799 self._monitoringRequested = True
800
801 def stopMonitoring(self):
802 """Stop the periodic monitoring of the aircraft."""
803 assert self._monitoringRequested
804 self._monitoringRequested = False
805
806 def startFlare(self):
807 """Start monitoring the flare time.
808
809 At present it is assumed to be called from the handler thread, hence no
810 protection."""
811 #self._aircraft.logger.debug("startFlare")
812 if self._flareRequestID is None:
813 self._flareRates = []
814 self._flareRequestID = \
815 self._handler.requestPeriodicRead(0.1,
816 Simulator.flareData1,
817 self._handleFlare1)
818
819 def cancelFlare(self):
820 """Cancel monitoring the flare time.
821
822 At present it is assumed to be called from the handler thread, hence no
823 protection."""
824 if self._flareRequestID is not None:
825 self._handler.clearPeriodic(self._flareRequestID)
826 self._flareRequestID = None
827
828 def sendMessage(self, message, duration = 3,
829 _disconnect = False):
830 """Send a message to the pilot via the simulator.
831
832 duration is the number of seconds to keep the message displayed."""
833 print("xplra.Simulator.sendMessage:", message)
834 self._handler.requestShowMessage(message, duration,
835 self._handleMessageSent,
836 extra = _disconnect)
837
838 def getFuel(self, callback):
839 """Get the fuel information for the current model.
840
841 The callback will be called with a list of triplets with the following
842 items:
843 - the fuel tank identifier
844 - the current weight of the fuel in the tank (in kgs)
845 - the current total capacity of the tank (in kgs)."""
846 if self._aircraftModel is None:
847 self._fuelCallback = callback
848 else:
849 self._aircraftModel.getFuel(self._handler, callback)
850
851 def setFuelLevel(self, levels):
852 """Set the fuel level to the given ones.
853
854 levels is an array of two-tuples, where each tuple consists of the
855 following:
856 - the const.FUELTANK_XXX constant denoting the tank that must be set,
857 - the requested level of the fuel as a floating-point value between 0.0
858 and 1.0."""
859 if self._aircraftModel is not None:
860 self._aircraftModel.setFuelLevel(self._handler, levels)
861
862 def enableTimeSync(self):
863 """Enable the time synchronization."""
864 self._nextSyncTime = -1
865 self._syncTime = True
866
867 def disableTimeSync(self):
868 """Enable the time synchronization."""
869 self._syncTime = False
870 self._nextSyncTime = -1
871
872 def listenHotkeys(self, hotkeys, callback):
873 """Start listening to the given hotkeys.
874
875 callback is function expecting two arguments:
876 - the ID of the hotkey set as returned by this function,
877 - the list of the indexes of the hotkeys that were pressed."""
878 with self._hotkeyLock:
879 assert self._hotkeyCodes is None
880
881 self._hotkeyCodes = \
882 [self._getHotkeyCode(hotkey) for hotkey in hotkeys]
883 self._hotkeySetID += 1
884 self._hotkeySetGeneration = 0
885 self._hotkeyCallback = callback
886
887 self._handler.registerHotkeys(self._hotkeyCodes,
888 self._handleHotkeysRegistered,
889 (self._hotkeySetID,
890 self._hotkeySetGeneration))
891
892 return self._hotkeySetID
893
894 def clearHotkeys(self):
895 """Clear the current hotkey set.
896
897 Note that it is possible, that the callback function set either
898 previously or after calling this function by listenHotkeys() will be
899 called with data from the previous hotkey set.
900
901 Therefore it is recommended to store the hotkey set ID somewhere and
902 check that in the callback function. Right before calling
903 clearHotkeys(), this stored ID should be cleared so that the check
904 fails for sure."""
905 with self._hotkeyLock:
906 if self._hotkeyCodes is not None:
907 self._hotkeyCodes = None
908 self._hotkeySetID += 1
909 self._hotkeyCallback = None
910 self._clearHotkeyRequest()
911
912 def disconnect(self, closingMessage = None, duration = 3):
913 """Disconnect from the simulator."""
914 assert not self._monitoringRequested
915
916 print("xplra.Simulator.disconnect", closingMessage, duration)
917
918 self._stopNormal()
919 self.clearHotkeys()
920 if closingMessage is None:
921 self._handler.disconnect()
922 else:
923 self.sendMessage(closingMessage, duration = duration,
924 _disconnect = True)
925
926 def connected(self, fsType, descriptor):
927 """Called when a connection has been established to the flight
928 simulator of the given type."""
929 self._fsType = fsType
930
931 with self._hotkeyLock:
932 if self._hotkeyCodes is not None:
933 self._hotkeySetGeneration += 1
934
935 self._handler.registerHotkeys(self._hotkeyCodes,
936 self._handleHotkeysRegistered,
937 (self._hotkeySetID,
938 self._hotkeySetGeneration))
939
940 self._connectionListener.connected(fsType, descriptor)
941
942 def connectionFailed(self):
943 """Called when the connection could not be established."""
944 with self._hotkeyLock:
945 self._clearHotkeyRequest()
946 self._connectionListener.connectionFailed()
947
948 def disconnected(self):
949 """Called when a connection to the flight simulator has been broken."""
950 with self._hotkeyLock:
951 self._clearHotkeyRequest()
952 self._connectionListener.disconnected()
953
954 def _getTimestamp(self, data):
955 """Convert the given data into a timestamp."""
956 if self._timestampBase is None:
957 year = datetime.date.today().year
958 self._timestampBase = \
959 calendar.timegm(time.struct_time([year, 1, 1, 0, 0, 0, -1, 1, 0]))
960 self._timestampBase += data[0] * 24 * 3600
961 self._timestampDaysOffset = 0
962 self._lastZuluSeconds = None
963
964 zuluSeconds = data[1]
965 if self._lastZuluSeconds is not None and \
966 zuluSeconds<self._lastZuluSeconds and \
967 self._monitoring:
968 diff = self._lastZuluSeconds - zuluSeconds
969 print("xplane.Simulator._getTimestamp: Zulu seconds have gone backwards: %f -> %f, diff: %f" % \
970 (self._lastZuluSeconds, zuluSeconds, diff))
971 if diff>23*60*60:
972 self._timestampDaysOffset += 1
973 else:
974 zuluSeconds = self._lastZuluSeconds
975
976 self._lastZuluSeconds = zuluSeconds
977
978 timestamp = self._timestampBase
979 timestamp += self._timestampDaysOffset * 24 * 3600
980 timestamp += zuluSeconds
981
982 return timestamp
983
984 def _startDefaultNormal(self):
985 """Start the default normal periodic request."""
986 assert self._normalRequestID is None
987 self._timestampBase = None
988 self._normalRequestID = \
989 self._handler.requestPeriodicRead(1.0,
990 Simulator.normalData,
991 self._handleNormal)
992
993 def _stopNormal(self):
994 """Stop the normal period request."""
995 assert self._normalRequestID is not None
996 self._handler.clearPeriodic(self._normalRequestID)
997 self._normalRequestID = None
998 self._monitoring = False
999
1000 def _handleNormal(self, data, extra):
1001 """Handle the reply to the normal request.
1002
1003 At the beginning the result consists the data for normalData. When
1004 monitoring is started, it contains the result also for the
1005 aircraft-specific values.
1006 """
1007 timestamp = self._getTimestamp(data)
1008
1009 createdNewModel = self._setAircraftName(timestamp,
1010 data.getString(2),
1011 data.getString(3),
1012 data.getString(4),
1013 data.getString(5),
1014 data.getString(6),
1015 data.getString(7))
1016 if self._fuelCallback is not None:
1017 self._aircraftModel.getFuel(self._handler, self._fuelCallback)
1018 self._fuelCallback = None
1019
1020 if self._monitoringRequested and not self._monitoring:
1021 self._stopNormal()
1022 self._startMonitoring()
1023 elif self._monitoring and not self._monitoringRequested:
1024 self._stopNormal()
1025 self._startDefaultNormal()
1026 elif self._monitoring and self._aircraftModel is not None and \
1027 not createdNewModel:
1028 aircraftState = self._aircraftModel.getAircraftState(self._aircraft,
1029 timestamp, data)
1030
1031 self._aircraft.handleState(aircraftState)
1032
1033 def _setAircraftName(self, timestamp, tailnum, author, description,
1034 notes, icao, liveryPath):
1035 """Set the name of the aicraft and if it is different from the
1036 previous, create a new model for it.
1037
1038 If so, also notifty the aircraft about the change.
1039
1040 Return if a new model was created."""
1041 aircraftInfo = (tailnum, author, description, notes, icao, liveryPath)
1042 if aircraftInfo==self._aircraftInfo:
1043 return False
1044
1045 print("xplane.Simulator: new data: %s, %s, %s, %s, %s, %s" % \
1046 (tailnum, author, description, notes, icao, liveryPath))
1047
1048 self._aircraftInfo = aircraftInfo
1049 needNew = self._aircraftModel is None
1050 needNew = needNew or\
1051 not self._aircraftModel.doesHandle(self._aircraft, aircraftInfo)
1052 if not needNew:
1053 specialModel = AircraftModel.findSpecial(self._aircraft,
1054 aircraftInfo)
1055 needNew = specialModel is not None and \
1056 specialModel is not self._aircraftModel.__class__
1057
1058 if needNew:
1059 self._setAircraftModel(AircraftModel.create(self._aircraft,
1060 aircraftInfo))
1061
1062 self._aircraft.modelChanged(timestamp, description,
1063 self._aircraftModel.name)
1064
1065 return needNew
1066
1067 def _setAircraftModel(self, model):
1068 """Set a new aircraft model.
1069
1070 It will be queried for the data to monitor and the monitoring request
1071 will be replaced by a new one."""
1072 self._aircraftModel = model
1073 model.simulator = self
1074
1075 if self._monitoring:
1076 self._stopNormal()
1077 self._startMonitoring()
1078
1079 def _startMonitoring(self):
1080 """Start monitoring with the current aircraft model."""
1081 data = Simulator.normalData[:]
1082 self._aircraftModel.addMonitoringData(data, self._fsType)
1083
1084 self._normalRequestID = \
1085 self._handler.requestPeriodicRead(1.0, data,
1086 self._handleNormal)
1087 self._monitoring = True
1088
1089 def _addFlareRate(self, data):
1090 """Append a flare rate to the list of last rates."""
1091 if len(self._flareRates)>=3:
1092 del self._flareRates[0]
1093 self._flareRates.append(data)
1094
1095 def _handleFlare1(self, data, normal):
1096 """Handle the first stage of flare monitoring."""
1097 #self._aircraft.logger.debug("handleFlare1: " + str(data))
1098 if data[1]<=50.0*0.3048:
1099 self._flareStart = time.time()
1100 self._flareStartFS = data[0]
1101 self._handler.clearPeriodic(self._flareRequestID)
1102 self._handler.requestRead(Simulator.flareStartData,
1103 self._handleFlareStart)
1104
1105 self._addFlareRate(data[2])
1106
1107 def _handleFlareStart(self, data, extra):
1108 """Handle the data need to notify the aircraft about the starting of
1109 the flare."""
1110 #self._aircraft.logger.debug("handleFlareStart: " + str(data))
1111 if data is not None:
1112 windDirection = data[1]
1113 if windDirection<0.0: windDirection += 360.0
1114 self._aircraft.flareStarted(data[0], windDirection, data[2],
1115 self._flareStart, self._flareStartFS)
1116
1117 self._flareRequestID = \
1118 self._handler.requestPeriodicRead(0.1,
1119 Simulator.flareData2,
1120 self._handleFlare2)
1121
1122 def _handleFlare2(self, data, normal):
1123 """Handle the first stage of flare monitoring."""
1124 #self._aircraft.logger.debug("handleFlare2: " + str(data))
1125 if data[1]!=0:
1126 flareEnd = time.time()
1127 self._handler.clearPeriodic(self._flareRequestID)
1128 self._flareRequestID = None
1129
1130 flareEndFS = data[0]
1131 if flareEndFS<self._flareStartFS:
1132 flareEndFS += 86400.0
1133
1134 tdRate = min(self._flareRates)
1135 tdRateCalculatedByFS = False
1136
1137 heading = data[6]
1138 if heading<0.0: heading += 360.0
1139
1140 self._aircraft.flareFinished(flareEnd, flareEndFS,
1141 tdRate, tdRateCalculatedByFS,
1142 data[3], data[4], data[5], heading)
1143 else:
1144 self._addFlareRate(data[2])
1145
1146 def _handleZFW(self, data, callback):
1147 """Callback for a ZFW retrieval request."""
1148 zfw = data[0] + data[1]
1149 callback(zfw)
1150
1151 def _handleTime(self, data, callback):
1152 """Callback for a time retrieval request."""
1153 callback(self._getTimestamp(data))
1154
1155 def _handleWeights(self, data, callback):
1156 """Callback for the weights retrieval request."""
1157 dow = data[0]
1158 payload = data[1]
1159 zfw = dow + payload
1160 grossWeight = data[2]
1161 callback(dow, payload, zfw, grossWeight)
1162
1163 def _handleMessageSent(self, success, disconnect):
1164 """Callback for a message sending request."""
1165 #print "xplra.Simulator._handleMessageSent", disconnect
1166 if disconnect:
1167 self._handler.disconnect()
1168
1169 def _handleHotkeysRegistered(self, success, hotkeySet):
1170 """Handle the result of the hotkeys having been written."""
1171 (id, generation) = hotkeySet
1172 with self._hotkeyLock:
1173 if success and id==self._hotkeySetID and \
1174 generation==self._hotkeySetGeneration:
1175 self._hotkeyRequestID = \
1176 self._handler.requestHotkeysState(0.5,
1177 self._handleHotkeys,
1178 hotkeySet)
1179
1180 def _handleHotkeys(self, data, hotkeySet):
1181 """Handle the hotkeys."""
1182 (id, generation) = hotkeySet
1183 with self._hotkeyLock:
1184 if id!=self._hotkeySetID or generation!=self._hotkeySetGeneration:
1185 return
1186
1187 callback = self._hotkeyCallback
1188 offsets = self._hotkeyOffets
1189
1190 hotkeysPressed = []
1191 for i in range(0, len(data)):
1192 if data[i]:
1193 hotkeysPressed.append(i)
1194
1195 if hotkeysPressed:
1196 callback(id, hotkeysPressed)
1197
1198 def _clearHotkeyRequest(self):
1199 """Clear the hotkey request in the handler if there is any."""
1200 if self._hotkeyRequestID is not None:
1201 self._handler.unregisterHotkeys(self._hotkeysUnregistered)
1202 self._handler.clearPeriodic(self._hotkeyRequestID)
1203 self._hotkeyRequestID = None
1204
1205 def _hotkeysUnregistered(self, result, extra):
1206 """Called when the hotkeys have been unregistered."""
1207 pass
1208
1209#------------------------------------------------------------------------------
1210
1211class AircraftModel(object):
1212 """Base class for the aircraft models.
1213
1214 Aircraft models handle the data arriving from X-Plane and turn it into an
1215 object describing the aircraft's state."""
1216 monitoringData = [ ("paused",
1217 "sim/time/paused", TYPE_INT),
1218 ("latitude",
1219 "sim/flightmodel/position/latitude", TYPE_DOUBLE),
1220 ("longitude",
1221 "sim/flightmodel/position/longitude", TYPE_DOUBLE),
1222 ("replay",
1223 "sim/operation/prefs/replay_mode", TYPE_INT),
1224 ("overspeed",
1225 "sim/flightmodel/failures/over_vne", TYPE_INT),
1226 ("stalled",
1227 "sim/flightmodel/failures/stallwarning", TYPE_INT),
1228 ("onTheGround",
1229 "sim/flightmodel/failures/onground_any", TYPE_INT),
1230 ("emptyWeight",
1231 "sim/aircraft/weight/acf_m_empty", TYPE_FLOAT),
1232 ("payloadWeight",
1233 "sim/flightmodel/weight/m_fixed", TYPE_FLOAT),
1234 ("grossWeight",
1235 "sim/flightmodel/weight/m_total", TYPE_FLOAT),
1236 ("heading",
1237 "sim/flightmodel/position/psi", TYPE_FLOAT),
1238 ("pitch",
1239 "sim/flightmodel/position/theta", TYPE_FLOAT),
1240 ("bank",
1241 "sim/flightmodel/position/phi", TYPE_FLOAT),
1242 ("ias",
1243 "sim/flightmodel/position/indicated_airspeed2",
1244 TYPE_FLOAT),
1245 ("mach",
1246 "sim/flightmodel/misc/machno", TYPE_FLOAT),
1247 ("groundSpeed",
1248 "sim/flightmodel/position/groundspeed", TYPE_FLOAT),
1249 ("vs",
1250 "sim/flightmodel/position/vh_ind_fpm2", TYPE_FLOAT),
1251 ("radioAltitude",
1252 "sim/flightmodel/position/y_agl", TYPE_FLOAT),
1253 ("altitude",
1254 "sim/flightmodel/position/elevation", TYPE_FLOAT),
1255 ("gLoad",
1256 "sim/flightmodel/forces/g_nrml", TYPE_FLOAT),
1257 ("flapsControl",
1258 "sim/flightmodel/controls/flaprqst", TYPE_FLOAT),
1259 ("flapsLeft",
1260 "sim/flightmodel/controls/flaprat", TYPE_FLOAT),
1261 ("flapsRight",
1262 "sim/flightmodel/controls/flap2rat", TYPE_FLOAT),
1263 ("navLights",
1264 "sim/cockpit/electrical/nav_lights_on", TYPE_INT),
1265 ("beaconLights",
1266 "sim/cockpit/electrical/beacon_lights_on", TYPE_INT),
1267 ("strobeLights",
1268 "sim/cockpit/electrical/strobe_lights_on", TYPE_INT),
1269 ("landingLights",
1270 "sim/cockpit/electrical/landing_lights_on", TYPE_INT),
1271 ("pitot",
1272 "sim/cockpit/switches/pitot_heat_on", TYPE_INT),
1273 ("parking",
1274 "sim/flightmodel/controls/parkbrake", TYPE_FLOAT),
1275 ("gearControl",
1276 "sim/cockpit2/controls/gear_handle_down", TYPE_INT),
1277 ("noseGear",
1278 "sim/flightmodel2/gear/deploy_ratio",
1279 (TYPE_FLOAT_ARRAY, 1)),
1280 ("spoilers",
1281 "sim/flightmodel/controls/lsplrdef", TYPE_FLOAT),
1282 ("altimeter",
1283 "sim/cockpit/misc/barometer_setting", TYPE_FLOAT),
1284 ("qnh",
1285 "sim/physics/earth_pressure_p", TYPE_FLOAT),
1286 ("nav1",
1287 "sim/cockpit/radios/nav1_freq_hz", TYPE_INT),
1288 ("nav1_obs",
1289 "sim/cockpit/radios/nav1_obs_degm", TYPE_FLOAT),
1290 ("nav2",
1291 "sim/cockpit/radios/nav2_freq_hz", TYPE_INT),
1292 ("nav2_obs",
1293 "sim/cockpit/radios/nav2_obs_degm", TYPE_FLOAT),
1294 ("adf1",
1295 "sim/cockpit/radios/adf1_freq_hz", TYPE_INT),
1296 ("adf2",
1297 "sim/cockpit/radios/adf2_freq_hz", TYPE_INT),
1298 ("squawk",
1299 "sim/cockpit/radios/transponder_code", TYPE_INT),
1300 ("windSpeed",
1301 "sim/weather/wind_speed_kt[0]", TYPE_FLOAT),
1302 ("windDirection",
1303 "sim/weather/wind_direction_degt[0]", TYPE_FLOAT),
1304 ("visibility",
1305 "sim/weather/visibility_reported_m", TYPE_FLOAT),
1306 ("cog",
1307 "sim/flightmodel/misc/cgz_ref_to_default", TYPE_FLOAT),
1308 ("xpdrC",
1309 "sim/cockpit/radios/transponder_mode", TYPE_INT),
1310 ("apMaster",
1311 "sim/cockpit/autopilot/autopilot_mode", TYPE_INT),
1312 ("apState",
1313 "sim/cockpit/autopilot/autopilot_state", TYPE_INT),
1314 ("apHeading",
1315 "sim/cockpit/autopilot/heading_mag", TYPE_FLOAT),
1316 ("apAltitude",
1317 "sim/cockpit/autopilot/altitude", TYPE_FLOAT),
1318 ("elevatorTrim",
1319 "sim/flightmodel/controls/elv_trim", TYPE_FLOAT),
1320 ("antiIceOn",
1321 "sim/cockpit/switches/anti_ice_on", TYPE_INT),
1322 ("surfaceHeat",
1323 "sim/cockpit/switches/anti_ice_surf_heat", TYPE_INT),
1324 ("propHeat",
1325 "sim/cockpit/switches/anti_ice_prop_heat", TYPE_INT),
1326 ("autopilotOn",
1327 "sim/cockpit2/autopilot/autopilot_on", TYPE_INT),
1328 ("apHeadingMode",
1329 "sim/cockpit2/autopilot/heading_mode", TYPE_INT)]
1330
1331
1332 specialModels = []
1333
1334 @staticmethod
1335 def registerSpecial(clazz):
1336 """Register the given class as a special model."""
1337 AircraftModel.specialModels.append(clazz)
1338
1339 @staticmethod
1340 def findSpecial(aircraft, aircraftInfo):
1341 for specialModel in AircraftModel.specialModels:
1342 if specialModel.doesHandle(aircraft, aircraftInfo):
1343 return specialModel
1344 return None
1345
1346 @staticmethod
1347 def create(aircraft, aircraftInfo):
1348 """Create the model for the given aircraft name, and notify the
1349 aircraft about it."""
1350 specialModel = AircraftModel.findSpecial(aircraft, aircraftInfo)
1351 if specialModel is not None:
1352 return specialModel()
1353 if aircraft.type in _genericModels:
1354 return _genericModels[aircraft.type]()
1355 else:
1356 return GenericModel()
1357
1358 @staticmethod
1359 def _convertFrequency(value):
1360 """Convert the given frequency value into a string."""
1361 return "%.2f" % (value/100.0,)
1362
1363 @staticmethod
1364 def _convertOBS(value):
1365 """Convert the given OBS value into an integer."""
1366 while value<0.0:
1367 value += 360.0
1368 return int(round(value))
1369
1370 def __init__(self, flapsNotches):
1371 """Construct the aircraft model.
1372
1373 flapsNotches is a list of degrees of flaps that are available on the aircraft."""
1374 self._flapsNotches = flapsNotches
1375 self._simulator = None
1376
1377 @property
1378 def name(self):
1379 """Get the name for this aircraft model."""
1380 return "X-Plane/Generic"
1381
1382 @property
1383 def simulator(self):
1384 """Get the simulator this aircraft model works for."""
1385 return self._simulator
1386
1387 @simulator.setter
1388 def simulator(self, simulator):
1389 """Get the simulator this aircraft model works for."""
1390 self._simulator = simulator
1391
1392 def doesHandle(self, aircraft, aircraftInfo):
1393 """Determine if the model handles the given aircraft name.
1394
1395 This default implementation returns False."""
1396 return False
1397
1398 def _addDatarefWithIndexMember(self, dest, name, type, attrName = None):
1399 """Add the given X-Plane dataref name and type to the given array and a
1400 member attribute with the given name."""
1401 dest.append((name, type))
1402 if attrName is not None:
1403 setattr(self, attrName, len(dest)-1)
1404
1405 def _addDataWithIndexMembers(self, dest, prefix, data):
1406 """Add X-Plane dataref data to the given array and also corresponding
1407 index member variables with the given prefix.
1408
1409 data is a list of triplets of the following items:
1410 - the name of the data item. The index member variable will have a name
1411 created by prepending the given prefix to this name.
1412 - the X-Plane dataref name
1413 - the dataref type
1414
1415 The latter two items will be appended to dest."""
1416 for (name, datarefName, type) in data:
1417 self._addDatarefWithIndexMember(dest, datarefName, type,
1418 prefix + name)
1419
1420 def addMonitoringData(self, data, fsType):
1421 """Add the model-specific monitoring data to the given array."""
1422 self._addDataWithIndexMembers(data, "_monidx_",
1423 AircraftModel.monitoringData)
1424
1425 def getAircraftState(self, aircraft, timestamp, data):
1426 """Get an aircraft state object for the given monitoring data."""
1427 state = fs.AircraftState()
1428
1429 lnavOn = data[self._monidx_autopilotOn]!=0 and \
1430 data[self._monidx_apHeadingMode]==2
1431
1432 state.timestamp = timestamp
1433
1434 state.latitude = data[self._monidx_latitude]
1435 state.longitude = data[self._monidx_longitude]
1436 if state.longitude>180.0: state.longitude = 360.0 - state.longitude
1437
1438 state.paused = data[self._monidx_paused]!=0 or \
1439 data[self._monidx_replay]!=0
1440 state.trickMode = data[self._monidx_replay]!=0
1441
1442 state.overspeed = data[self._monidx_overspeed]!=0
1443 state.stalled = data[self._monidx_stalled]!=0
1444 state.onTheGround = data[self._monidx_onTheGround]!=0
1445
1446 state.zfw = data[self._monidx_emptyWeight] + \
1447 data[self._monidx_payloadWeight]
1448 state.grossWeight = data[self._monidx_grossWeight]
1449
1450 state.heading = data[self._monidx_heading]
1451
1452 state.pitch = -1.0 * data[self._monidx_pitch]
1453 state.bank = data[self._monidx_bank]
1454
1455 state.ias = data[self._monidx_ias]
1456 state.mach = data[self._monidx_mach]
1457 state.groundSpeed = data[self._monidx_groundSpeed] * _mps2knots
1458 state.vs = data[self._monidx_vs]
1459
1460 state.radioAltitude = data[self._monidx_radioAltitude]/.3048
1461 state.altitude = data[self._monidx_altitude]/.3048
1462
1463 state.gLoad = data[self._monidx_gLoad]
1464
1465 flapsControl = data[self._monidx_flapsControl]
1466 flapsIndex = int(round(flapsControl * (len(self._flapsNotches)-1)))
1467 state.flapsSet = 0 if flapsIndex<1 else self._flapsNotches[flapsIndex]
1468
1469 state.flaps = self._flapsNotches[-1]*data[self._monidx_flapsLeft]
1470
1471 state.navLightsOn = data[self._monidx_navLights] != 0
1472 state.antiCollisionLightsOn = data[self._monidx_beaconLights] != 0
1473 state.landingLightsOn = data[self._monidx_landingLights] != 0
1474 state.strobeLightsOn = data[self._monidx_strobeLights] != 0
1475
1476 state.pitotHeatOn = data[self._monidx_pitot]!=0
1477
1478 state.parking = data[self._monidx_parking]>=0.5
1479
1480 state.gearControlDown = data[self._monidx_gearControl]!=0
1481 state.gearsDown = data[self._monidx_noseGear][0]>0.99
1482
1483 state.spoilersArmed = None
1484
1485 state.spoilersExtension = data[self._monidx_spoilers]*100.0
1486
1487 state.altimeter = data[self._monidx_altimeter]* _hgin2hpa
1488 state.altimeterReliable = True
1489 state.qnh = data[self._monidx_qnh]/100.0
1490
1491 state.ils = None
1492 state.ils_obs = None
1493 state.ils_manual = False
1494 state.nav1 = self._convertFrequency(data[self._monidx_nav1])
1495 state.nav1_obs = self._convertOBS(data[self._monidx_nav1_obs])
1496 state.nav1_manual = True
1497 state.nav2 = self._convertFrequency(data[self._monidx_nav2])
1498 state.nav2_obs = self._convertOBS(data[self._monidx_nav2_obs])
1499 state.nav2_manual = not lnavOn
1500 state.adf1 = str(data[self._monidx_adf1])
1501 state.adf2 = str(data[self._monidx_adf2])
1502
1503 state.squawk = "%04d" % (data[self._monidx_squawk],)
1504
1505 state.windSpeed = data[self._monidx_windSpeed]
1506 state.windDirection = data[self._monidx_windDirection]
1507 if state.windDirection<0.0: state.windDirection += 360.0
1508
1509 state.visibility = data[self._monidx_visibility]
1510
1511 state.cog = data[self._monidx_cog]
1512
1513 state.xpdrC = data[self._monidx_xpdrC]>=2
1514 state.autoXPDR = False
1515
1516 state.apMaster = data[self._monidx_apMaster]==2
1517 apState = data[self._monidx_apState]
1518 if lnavOn:
1519 state.apHeadingHold = None
1520 state.apHeading = None
1521 else:
1522 state.apHeadingHold = (apState&0x00002)!=0
1523 state.apHeading = data[self._monidx_apHeading]
1524
1525 state.apAltitudeHold = (apState&0x04000)!=0
1526 state.apAltitude = data[self._monidx_apAltitude]
1527
1528 state.elevatorTrim = data[self._monidx_elevatorTrim] * 180.0 / math.pi
1529
1530 state.antiIceOn = data[self._monidx_antiIceOn]!=0 or \
1531 data[self._monidx_surfaceHeat]!=0 or \
1532 data[self._monidx_propHeat]!=0
1533
1534 return state
1535
1536#------------------------------------------------------------------------------
1537
1538class GenericAircraftModel(AircraftModel):
1539 """A generic aircraft model that can handle the fuel levels, the N1 or RPM
1540 values and some other common parameters in a generic way."""
1541
1542 def __init__(self, flapsNotches, fuelTanks, numEngines, isN1 = True):
1543 """Construct the generic aircraft model with the given data.
1544
1545 flapsNotches is an array of how much degrees the individual flaps
1546 notches mean.
1547
1548 fuelTanks is an array of const.FUELTANK_XXX constants about the
1549 aircraft's fuel tanks. They will be converted to offsets.
1550
1551 numEngines is the number of engines the aircraft has.
1552
1553 isN1 determines if the engines have an N1 value or an RPM value
1554 (e.g. pistons)."""
1555 super(GenericAircraftModel, self).__init__(flapsNotches = flapsNotches)
1556
1557 self._fuelTanks = fuelTanks
1558 self._fuelTankCapacities = [1.0] * len(fuelTanks)
1559 self._fuelIndex = None
1560 self._numEngines = numEngines
1561 self._engineStartIndex = None
1562 self._isN1 = isN1
1563
1564 def doesHandle(self, aircraft, aircraftInfo):
1565 """Determine if the model handles the given aircraft name.
1566
1567 This implementation returns True."""
1568 return True
1569
1570 def addMonitoringData(self, data, fsType):
1571 """Add the model-specific monitoring data to the given array."""
1572 super(GenericAircraftModel, self).addMonitoringData(data, fsType)
1573
1574 self._fuelIndex = self._addFuelData(data)
1575
1576 self._engineStartIndex = len(data)
1577 if self._isN1:
1578 self._addDatarefWithIndexMember(data,
1579 "sim/flightmodel/engine/ENGN_N1_",
1580 (TYPE_FLOAT_ARRAY,
1581 self._numEngines))
1582 else:
1583 self._addDatarefWithIndexMember(data,
1584 "sim/flightmodel/engine/POINT_tacrad",
1585 (TYPE_FLOAT_ARRAY,
1586 self._numEngines))
1587
1588 self._addDatarefWithIndexMember(data,
1589 "sim/flightmodel/engine/ENGN_propmode",
1590 (TYPE_INT_ARRAY, self._numEngines))
1591
1592 def getAircraftState(self, aircraft, timestamp, data):
1593 """Get the aircraft state.
1594
1595 Get it from the parent, and then add the data about the fuel levels and
1596 the engine parameters."""
1597 state = super(GenericAircraftModel, self).getAircraftState(aircraft,
1598 timestamp,
1599 data)
1600
1601 state.fuel = []
1602 state.totalFuel = 0.0
1603
1604 fuelAmounts = data[self._fuelIndex]
1605 for i in range(0, len(self._fuelTanks)):
1606 amount = fuelAmounts[i]
1607 state.fuel.append((self._fuelTanks[i], amount))
1608 state.totalFuel += amount
1609
1610 power = data[self._engineStartIndex]
1611
1612 state.n1 = power[:] if self._isN1 else None
1613 state.rpm = None if self._isN1 else power[:]
1614
1615 propMode = data[self._engineStartIndex+1]
1616 state.reverser = [mode == 3 for mode in propMode]
1617
1618 return state
1619
1620 def getFuel(self, handler, callback):
1621 """Get the fuel information for this model.
1622
1623 See Simulator.getFuel for more information. This
1624 implementation simply queries the fuel tanks given to the
1625 constructor."""
1626 data = []
1627 self._addFuelData(data)
1628 data.append( ("sim/aircraft/weight/acf_m_fuel_tot", TYPE_FLOAT) )
1629 data.append( ("sim/aircraft/overflow/acf_tank_rat",
1630 (TYPE_FLOAT_ARRAY, len(self._fuelTanks)) ) )
1631
1632 handler.requestRead(data, self._handleFuelRetrieved,
1633 extra = callback)
1634
1635 def setFuelLevel(self, handler, levels):
1636 """Set the fuel level.
1637
1638 See the description of Simulator.setFuelLevel. This
1639 implementation simply sets the fuel tanks as given."""
1640 data = []
1641 for (tank, level) in levels:
1642 try:
1643 index = self._fuelTanks.index(tank)
1644 data.append( ("sim/flightmodel/weight/m_fuel",
1645 (TYPE_FLOAT_ARRAY, 1, index),
1646 [level * self._fuelTankCapacities[index]]) )
1647 except:
1648 print("xplane.Simulator.setFuelLevel: invalid tank constant: %d" % \
1649 (tank,))
1650
1651 handler.requestWrite(data, self._handleFuelWritten)
1652
1653 def _addFuelData(self, data):
1654 """Add the fuel offsets to the given data array.
1655
1656 Returns the index of the first fuel tank's data."""
1657 fuelStartIndex = len(data)
1658 data.append( ("sim/flightmodel/weight/m_fuel",
1659 (TYPE_FLOAT_ARRAY, len(self._fuelTanks) ) ) )
1660
1661 return fuelStartIndex
1662
1663 def _convertFuelData(self, data, index = 0, addCapacities = False):
1664 """Convert the given data into a fuel info list.
1665
1666 The list consists of two or three-tuples of the following
1667 items:
1668 - the fuel tank ID,
1669 - the amount of the fuel in kg,
1670 - if addCapacities is True, the total capacity of the tank."""
1671 fuelWeight = data[index] / 256.0
1672 index += 1
1673
1674 result = []
1675 totalFuel = 0
1676 for fuelTank in self._fuelTanks:
1677 capacity = data[index+1] * fuelWeight * const.LBSTOKG
1678 if capacity>=1.0:
1679 amount = data[index] * capacity / 128.0 / 65536.0
1680
1681 result.append( (fuelTank, amount, capacity) if addCapacities
1682 else (fuelTank, amount))
1683 totalFuel += amount
1684 index += 2
1685
1686 return (result, totalFuel)
1687
1688 def _handleFuelRetrieved(self, data, callback):
1689 """Callback for a fuel retrieval request."""
1690 result = []
1691 totalCapacity = data[1]
1692 for index in range(0, len(self._fuelTanks)):
1693 amount = data[0][index]
1694 capacity = data[2][index] * totalCapacity
1695 self._fuelTankCapacities[index] = capacity
1696 result.append( (self._fuelTanks[index], amount, capacity) )
1697
1698 callback(result)
1699
1700 def _handleFuelWritten(self, success, extra):
1701 """Callback for a fuel setting request."""
1702 pass
1703
1704#------------------------------------------------------------------------------
1705
1706class GenericModel(GenericAircraftModel):
1707 """Generic aircraft model for an unknown type."""
1708 def __init__(self):
1709 """Construct the model."""
1710 super(GenericModel, self). \
1711 __init__(flapsNotches = [0, 10, 20, 30],
1712 fuelTanks = [const.FUELTANK_LEFT, const.FUELTANK_RIGHT],
1713 numEngines = 2)
1714
1715 @property
1716 def name(self):
1717 """Get the name for this aircraft model."""
1718 return "X-Plane/Generic"
1719
1720#------------------------------------------------------------------------------
1721
1722class B737Model(GenericAircraftModel):
1723 """Generic model for the Boeing 737 Classing and NG aircraft."""
1724 fuelTanks = [const.FUELTANK_LEFT, const.FUELTANK_CENTRE, const.FUELTANK_RIGHT]
1725
1726 def __init__(self):
1727 """Construct the model."""
1728 super(B737Model, self). \
1729 __init__(flapsNotches = [0, 1, 2, 5, 10, 15, 25, 30, 40],
1730 fuelTanks = B737Model.fuelTanks,
1731 numEngines = 2)
1732
1733 @property
1734 def name(self):
1735 """Get the name for this aircraft model."""
1736 return "X-Plane/Generic Boeing 737"
1737
1738#------------------------------------------------------------------------------
1739
1740class ZiboB737NGModel(B737Model):
1741 """Base model for the Zibo and LevelUp Boeing 737 models."""
1742 def __init__(self, flapsRatios = [0.0, 0.081633, 0.142857, 0.224490,
1743 0.285714, 0.367347, 0.551020, 0.714286,
1744 1.0]):
1745 super(ZiboB737NGModel, self).__init__()
1746 self._flapsRatios = flapsRatios
1747
1748 def addMonitoringData(self, data, fsType):
1749 """Add the model-specific monitoring data to the given array."""
1750 super(ZiboB737NGModel, self).addMonitoringData(data, fsType)
1751
1752 self._speedBrakeIndex = len(data)
1753 self._addDatarefWithIndexMember(data,
1754 "sim/flightmodel2/wing/speedbrake1_deg",
1755 (TYPE_FLOAT_ARRAY, 2))
1756 self._addDatarefWithIndexMember(data,
1757 "sim/flightmodel2/wing/speedbrake2_deg",
1758 (TYPE_FLOAT_ARRAY, 2))
1759 self._cgIndex = len(data)
1760 self._addDatarefWithIndexMember(data,
1761 "laminar/B738/tab/cg_pos",
1762 TYPE_FLOAT)
1763 self._wingHeatIndex = len(data)
1764 self._addDatarefWithIndexMember(data,
1765 "laminar/B738/ice/wing_heat_pos",
1766 TYPE_FLOAT)
1767 self._eng1HeatIndex = len(data)
1768 self._addDatarefWithIndexMember(data,
1769 "laminar/B738/ice/eng1_heat_pos",
1770 TYPE_FLOAT)
1771 self._eng2HeatIndex = len(data)
1772 self._addDatarefWithIndexMember(data,
1773 "laminar/B738/ice/eng2_heat_pos",
1774 TYPE_FLOAT)
1775 self._spoilersArmedIndex = len(data)
1776 self._addDatarefWithIndexMember(data,
1777 "laminar/B738/annunciator/speedbrake_armed",
1778 TYPE_FLOAT)
1779
1780 self._apCMDStatusIndex = len(data)
1781 self._addDatarefWithIndexMember(data,
1782 "laminar/B738/autopilot/cmd_a_status",
1783 TYPE_FLOAT)
1784 self._addDatarefWithIndexMember(data,
1785 "laminar/B738/autopilot/cmd_b_status",
1786 TYPE_FLOAT)
1787
1788 self._apHeadingIndex = len(data)
1789 self._addDatarefWithIndexMember(data,
1790 "laminar/B738/autopilot/hdg_sel_status",
1791 TYPE_FLOAT)
1792 self._addDatarefWithIndexMember(data,
1793 "laminar/B738/hud/hdg_bug_tape",
1794 TYPE_FLOAT)
1795
1796 self._apAltitudeIndex = len(data)
1797 self._addDatarefWithIndexMember(data,
1798 "laminar/B738/autopilot/alt_hld_status",
1799 TYPE_FLOAT)
1800
1801
1802 def getAircraftState(self, aircraft, timestamp, data):
1803 """Get the aircraft state."""
1804 state = super(ZiboB737NGModel, self).getAircraftState(aircraft,
1805 timestamp,
1806 data)
1807 state.cog = data[self._cgIndex]/100.0
1808
1809 flapsRatios = self._flapsRatios
1810 flapsRatio = data[self._monidx_flapsLeft]
1811 index = len(flapsRatios)
1812 for i in range(1, len(flapsRatios)):
1813 if flapsRatio<flapsRatios[i]:
1814 index = i-1
1815 break
1816 if index<len(flapsRatios):
1817 flapsRatio0 = flapsRatios[index]
1818 flapsNotch0 = self._flapsNotches[index]
1819 state.flaps = flapsNotch0 + \
1820 (self._flapsNotches[index+1] - flapsNotch0) * \
1821 (flapsRatio - flapsRatio0) / \
1822 (flapsRatios[index+1] - flapsRatio0)
1823 else:
1824 state.flaps = self._flapsNotches[-1]
1825
1826 # 0 -> -1
1827 # 15 -> 0.790881
1828 state.elevatorTrim = \
1829 15.0 * (data[self._monidx_elevatorTrim] + 1) / 1.790881
1830
1831 state.spoilersExtension = \
1832 sum(data[self._speedBrakeIndex] + data[self._speedBrakeIndex+1])/4
1833
1834 state.antiIceOn = data[self._wingHeatIndex]!=0 or \
1835 data[self._eng1HeatIndex]!=0 or \
1836 data[self._eng2HeatIndex]!=0
1837
1838 state.spoilersArmed = data[self._spoilersArmedIndex]!=0
1839
1840 state.apMaster = \
1841 data[self._apCMDStatusIndex]==1 or \
1842 data[self._apCMDStatusIndex+1]==1
1843
1844 mcpHeadingHoldStatus = data[self._apHeadingIndex]
1845 mcpHeadingBugStatus = data[self._apHeadingIndex+1]
1846 state.apHeadingHold = mcpHeadingHoldStatus!=0 and \
1847 (mcpHeadingBugStatus<=0.5 and mcpHeadingBugStatus>=-0.5)
1848
1849 state.apAltitudeHold = data[self._apAltitudeIndex]==1
1850
1851 return state
1852
1853#------------------------------------------------------------------------------
1854
1855class ZiboB738Model(ZiboB737NGModel):
1856 """Model for the Zibo Boeing 737-800 model."""
1857 @staticmethod
1858 def doesHandle(aircraft, data):
1859 """Determine if this model handler handles the aircraft with the given
1860 name."""
1861 (tailnum, author, description, notes, icao, liveryPath) = data
1862 return author=="Alex Unruh" and \
1863 description=="Boeing 737-800X" and \
1864 notes.startswith("ZIBOmod") and \
1865 icao=="B738"
1866
1867 def __init__(self):
1868 """Construct the model."""
1869 super(ZiboB738Model, self).__init__(
1870 flapsRatios = [0.0, 0.081633, 0.142857, 0.224490, 0.285714, 0.346939,
1871 0.551020, 0.673469, 1.0])
1872
1873 @property
1874 def name(self):
1875 """Get the name for this aircraft model."""
1876 return "Zibo Boeing 737-800"
1877
1878#------------------------------------------------------------------------------
1879
1880class LevelUpB736Model(ZiboB737NGModel):
1881 """Model for the LevelUp Boeing 737-600 model."""
1882
1883 @staticmethod
1884 def doesHandle(aircraft, data):
1885 """Determine if this model handler handles the aircraft with the given
1886 name."""
1887 (tailnum, author, description, notes, icao, liveryPath) = data
1888 return author=="Alex Unruh" and \
1889 description=="Boeing 737-600NG" and \
1890 icao=="B736"
1891
1892 @property
1893 def name(self):
1894 """Get the name for this aircraft model."""
1895 return "LevelUp Boeing 737-600"
1896
1897#------------------------------------------------------------------------------
1898
1899class LevelUpB737Model(ZiboB737NGModel):
1900 """Model for the LevelUp Boeing 737-700 model."""
1901
1902 @staticmethod
1903 def doesHandle(aircraft, data):
1904 """Determine if this model handler handles the aircraft with the given
1905 name."""
1906 (tailnum, author, description, notes, icao, liveryPath) = data
1907 return author=="Alex Unruh" and \
1908 description=="Boeing 737-700NG" and \
1909 icao=="B737"
1910
1911 @property
1912 def name(self):
1913 """Get the name for this aircraft model."""
1914 return "LevelUp Boeing 737-700"
1915
1916#------------------------------------------------------------------------------
1917
1918class LevelUpB738Model(ZiboB737NGModel):
1919 """Model for the LevelUp Boeing 737-800 model."""
1920
1921 @staticmethod
1922 def doesHandle(aircraft, data):
1923 """Determine if this model handler handles the aircraft with the given
1924 name."""
1925 (tailnum, author, description, notes, icao, liveryPath) = data
1926 return author=="Alex Unruh" and \
1927 description=="Boeing 737-800NG" and \
1928 icao=="B738"
1929
1930 @property
1931 def name(self):
1932 """Get the name for this aircraft model."""
1933 return "LevelUp Boeing 737-800"
1934
1935#------------------------------------------------------------------------------
1936
1937class B767Model(GenericAircraftModel):
1938 """Generic model for the Boeing 767 aircraft."""
1939 fuelTanks = [const.FUELTANK_LEFT, const.FUELTANK_CENTRE, const.FUELTANK_RIGHT]
1940
1941 def __init__(self):
1942 """Construct the model."""
1943 super(B767Model, self). \
1944 __init__(flapsNotches = [0, 1, 5, 15, 20, 25, 30],
1945 fuelTanks = B767Model.fuelTanks,
1946 numEngines = 2)
1947
1948 @property
1949 def name(self):
1950 """Get the name for this aircraft model."""
1951 return "X-Plane/Generic Boeing 767"
1952
1953#------------------------------------------------------------------------------
1954
1955class DH8DModel(GenericAircraftModel):
1956 """Generic model for the Bombardier Dash 8-Q400 aircraft."""
1957 fuelTanks = [const.FUELTANK_LEFT, const.FUELTANK_RIGHT]
1958
1959 def __init__(self):
1960 """Construct the model."""
1961 super(DH8DModel, self). \
1962 __init__(flapsNotches = [0, 5, 10, 15, 35],
1963 fuelTanks = DH8DModel.fuelTanks,
1964 numEngines = 2)
1965
1966 @property
1967 def name(self):
1968 """Get the name for this aircraft model."""
1969 return "X-Plane/Generic Bombardier Dash 8-Q400"
1970
1971#------------------------------------------------------------------------------
1972
1973class FJSDH8DModel(DH8DModel):
1974 """Model handler for the FlyJSim Dash 8-Q400."""
1975 @staticmethod
1976 def doesHandle(aircraft, data):
1977 """Determine if this model handler handles the aircraft with the given
1978 name."""
1979 (tailnum, author, description, notes, icao, liveryPath) = data
1980 return aircraft.type==const.AIRCRAFT_DH8D and \
1981 description.find("Dash 8 Q400")!=-1 and \
1982 ((author in ["2012", "2013"] and tailnum=="N62890") or \
1983 author.find("Jack Skieczius")!=-1)
1984
1985 @property
1986 def name(self):
1987 """Get the name for this aircraft model."""
1988 return "X-Plane/FlyJSim Bombardier Dash 8-Q400"
1989
1990 def addMonitoringData(self, data, fsType):
1991 """Add the model-specific monitoring data to the given array."""
1992 super(FJSDH8DModel, self).addMonitoringData(data, fsType)
1993
1994 self._speedBrakeIndex = len(data)
1995 self._addDatarefWithIndexMember(data,
1996 "sim/flightmodel2/wing/speedbrake1_deg",
1997 (TYPE_FLOAT_ARRAY, 2))
1998 self._addDatarefWithIndexMember(data,
1999 "sim/flightmodel2/wing/speedbrake2_deg",
2000 (TYPE_FLOAT_ARRAY, 2))
2001
2002
2003 def getAircraftState(self, aircraft, timestamp, data):
2004 """Get the aircraft state.
2005
2006 Get it from the parent, and then invert the pitot heat state."""
2007 state = super(FJSDH8DModel, self).getAircraftState(aircraft,
2008 timestamp,
2009 data)
2010 state.antiCollisionLightsOn = \
2011 state.antiCollisionLightsOn or state.strobeLightsOn
2012 state.cog = (state.cog / 0.0254 + 21.504) / 94.512
2013
2014 # It seems that N1 does not always go down to 0 properly
2015 # (maybe due to winds?)
2016 state.n1 = [0 if n1<2.0 else n1 for n1 in state.n1]
2017
2018 state.spoilersExtension = \
2019 sum(data[self._speedBrakeIndex] + data[self._speedBrakeIndex+1])/4
2020
2021 return state
2022
2023#------------------------------------------------------------------------------
2024
2025class FJSDH8DXPModel(DH8DModel):
2026 """Model handler for the FlyJSim Q4XP."""
2027 @staticmethod
2028 def doesHandle(aircraft, data):
2029 """Determine if this model handler handles the aircraft with the given
2030 name."""
2031 (tailnum, author, description, notes, icao, liveryPath) = data
2032 return aircraft.type==const.AIRCRAFT_DH8D and \
2033 description.find("Dash 8 Q400")!=-1 and \
2034 author=="FlyJSim" and tailnum=="N62890"
2035
2036 @property
2037 def name(self):
2038 """Get the name for this aircraft model."""
2039 return "X-Plane/FlyJSim Q4XP"
2040
2041 def addMonitoringData(self, data, fsType):
2042 """Add the model-specific monitoring data to the given array."""
2043 super(FJSDH8DXPModel, self).addMonitoringData(data, fsType)
2044
2045 self._speedBrakeIndex = len(data)
2046 self._addDatarefWithIndexMember(data,
2047 "sim/flightmodel2/wing/spoiler1_deg",
2048 (TYPE_FLOAT_ARRAY, 32))
2049 self._addDatarefWithIndexMember(data,
2050 "sim/flightmodel2/wing/spoiler2_deg",
2051 (TYPE_FLOAT_ARRAY, 32))
2052
2053 self._gearIndex = len(data)
2054 self._addDatarefWithIndexMember(data,
2055 "FJS/Q4XP/Manips/GearDeployHandle_Ctl",
2056 (TYPE_FLOAT_ARRAY, 1))
2057
2058 self._apIndex = len(data)
2059 self._addDatarefWithIndexMember(data,
2060 "FJS/Q4XP/FMA/roll_act",
2061 TYPE_INT)
2062 self._addDatarefWithIndexMember(data,
2063 "FJS/Q4XP/FMA/pitch_act",
2064 TYPE_INT)
2065
2066 self._propPitchIndex = len(data)
2067 self._addDatarefWithIndexMember(data,
2068 "sim/flightmodel2/engines/prop_pitch_deg",
2069 (TYPE_FLOAT_ARRAY, 2))
2070
2071
2072 def getAircraftState(self, aircraft, timestamp, data):
2073 """Get the aircraft state.
2074
2075 Get it from the parent, and then invert the pitot heat state."""
2076 state = super(FJSDH8DXPModel, self).getAircraftState(aircraft,
2077 timestamp,
2078 data)
2079 state.antiCollisionLightsOn = \
2080 state.antiCollisionLightsOn or state.strobeLightsOn
2081 state.cog = (state.cog * 41.656436697 + 27.586779769)/100.0
2082
2083 # It seems that N1 does not always go down to 0 properly
2084 # (maybe due to winds?)
2085 state.n1 = [0 if n1<2.0 else n1 for n1 in state.n1]
2086
2087 state.spoilersExtension = \
2088 sum(data[self._speedBrakeIndex] + data[self._speedBrakeIndex+1])/4
2089 if state.spoilersExtension<40:
2090 state.spoilersExtension = 0.0
2091
2092 state.gearControlDown = data[self._gearIndex][0]>0.5
2093
2094 state.apHeadingHold = data[self._apIndex]==4
2095 state.apAltitudeHold = data[self._apIndex+1] in [4, 5]
2096
2097 state.reverser = [p<=-12.0 for p in data[self._propPitchIndex]]
2098
2099 return state
2100
2101#------------------------------------------------------------------------------
2102
2103class CRJ2Model(GenericAircraftModel):
2104 """Generic model for the Bombardier CRJ-200 aircraft."""
2105 fuelTanks = [const.FUELTANK_LEFT, const.FUELTANK_CENTRE, const.FUELTANK_RIGHT]
2106
2107 def __init__(self):
2108 """Construct the model."""
2109 super(CRJ2Model, self). \
2110 __init__(flapsNotches = [0, 8, 20, 30, 45],
2111 fuelTanks = CRJ2Model.fuelTanks,
2112 numEngines = 2)
2113
2114 @property
2115 def name(self):
2116 """Get the name for this aircraft model."""
2117 return "X-Plane/Generic Bombardier CRJ-200"
2118
2119#------------------------------------------------------------------------------
2120
2121class F70Model(GenericAircraftModel):
2122 """Generic model for the Fokker F70 aircraft."""
2123 fuelTanks = [const.FUELTANK_LEFT, const.FUELTANK_CENTRE, const.FUELTANK_RIGHT]
2124
2125 def __init__(self):
2126 """Construct the model."""
2127 super(F70Model, self). \
2128 __init__(flapsNotches = [0, 8, 15, 25, 42],
2129 fuelTanks = F70Model.fuelTanks,
2130 numEngines = 2)
2131
2132 @property
2133 def name(self):
2134 """Get the name for this aircraft model."""
2135 return "X-Plane/Generic Fokker 70"
2136
2137#------------------------------------------------------------------------------
2138
2139class DC3Model(GenericAircraftModel):
2140 """Generic model for the Lisunov Li-2 (DC-3) aircraft."""
2141 fuelTanks = [const.FUELTANK_LEFT, const.FUELTANK_CENTRE,
2142 const.FUELTANK_RIGHT]
2143 # fuelTanks = [const.FUELTANK_LEFT_AUX, const.FUELTANK_LEFT,
2144 # const.FUELTANK_RIGHT, const.FUELTANK_RIGHT_AUX]
2145
2146 def __init__(self):
2147 """Construct the model."""
2148 super(DC3Model, self). \
2149 __init__(flapsNotches = [0, 15, 30, 45],
2150 fuelTanks = DC3Model.fuelTanks,
2151 numEngines = 2, isN1 = False)
2152 self._leftLevel = 0.0
2153 self._rightLevel = 0.0
2154
2155 @property
2156 def name(self):
2157 """Get the name for this aircraft model."""
2158 return "X-Plane/Generic Lisunov Li-2 (DC-3)"
2159
2160#------------------------------------------------------------------------------
2161
2162class T134Model(GenericAircraftModel):
2163 """Generic model for the Tupolev Tu-134 aircraft."""
2164 fuelTanks = [const.FUELTANK_LEFT_TIP, const.FUELTANK_EXTERNAL1,
2165 const.FUELTANK_LEFT_AUX,
2166 const.FUELTANK_CENTRE,
2167 const.FUELTANK_RIGHT_AUX,
2168 const.FUELTANK_EXTERNAL2, const.FUELTANK_RIGHT_TIP]
2169
2170 def __init__(self):
2171 """Construct the model."""
2172 super(T134Model, self). \
2173 __init__(flapsNotches = [0, 10, 20, 30],
2174 fuelTanks = T134Model.fuelTanks,
2175 numEngines = 2)
2176
2177 @property
2178 def name(self):
2179 """Get the name for this aircraft model."""
2180 return "X-Plane/Generic Tupolev Tu-134"
2181
2182#------------------------------------------------------------------------------
2183
2184class T154Model(GenericAircraftModel):
2185 """Generic model for the Tupolev Tu-154 aircraft."""
2186 fuelTanks = [const.FUELTANK_CENTRE, const.FUELTANK_CENTRE2,
2187 const.FUELTANK_RIGHT, const.FUELTANK_LEFT,
2188 const.FUELTANK_RIGHT_AUX, const.FUELTANK_LEFT_AUX]
2189
2190 def __init__(self):
2191 """Construct the model."""
2192 super(T154Model, self). \
2193 __init__(flapsNotches = [0, 15, 28, 45],
2194 fuelTanks = T154Model.fuelTanks,
2195 numEngines = 3)
2196
2197 @property
2198 def name(self):
2199 """Get the name for this aircraft model."""
2200 return "X-Plane/Generic Tupolev Tu-154"
2201
2202 def getAircraftState(self, aircraft, timestamp, data):
2203 """Get an aircraft state object for the given monitoring data.
2204
2205 This removes the reverser value for the middle engine."""
2206 state = super(T154Model, self).getAircraftState(aircraft, timestamp, data)
2207 del state.reverser[1]
2208 return state
2209
2210#------------------------------------------------------------------------------
2211
2212class FelisT154Model(T154Model):
2213 """Model for Felis' Tupolev Tu-154-M aircraft."""
2214 @staticmethod
2215 def doesHandle(aircraft, data):
2216 """Determine if this model handler handles the aircraft with the given
2217 name."""
2218 (tailnum, author, description, notes, icao, liveryPath) = data
2219 return aircraft.type==const.AIRCRAFT_T154 and \
2220 author.find("Felis")!=-1 and \
2221 description.find("Tu154M")!=-1
2222
2223 def __init__(self):
2224 """Construct the model."""
2225 super(T154Model, self). \
2226 __init__(flapsNotches = [0, 15, 28, 36, 45],
2227 fuelTanks = T154Model.fuelTanks,
2228 numEngines = 3)
2229
2230 @property
2231 def name(self):
2232 """Get the name for this aircraft model."""
2233 return "X-Plane/Felis Tupolev Tu-154-M"
2234
2235#------------------------------------------------------------------------------
2236
2237class FelisT154B2Model(T154Model):
2238 """Model for Felis' Tupolev Tu-154-B2 aircraft."""
2239 @staticmethod
2240 def doesHandle(aircraft, data):
2241 """Determine if this model handler handles the aircraft with the given
2242 name."""
2243 (tailnum, author, description, notes, icao, liveryPath) = data
2244 return aircraft.type==const.AIRCRAFT_T154 and \
2245 author.find("Felis")!=-1 and \
2246 description.find("Tu154B2")!=-1
2247
2248 @property
2249 def name(self):
2250 """Get the name for this aircraft model."""
2251 return "X-Plane/Felis Tupolev Tu-154-B2"
2252
2253 def addMonitoringData(self, data, fsType):
2254 """Add the model-specific monitoring data to the given array."""
2255 super(FelisT154B2Model, self).addMonitoringData(data, fsType)
2256
2257 self._parkingBrakeIndex = len(data)
2258 self._addDatarefWithIndexMember(data,
2259 "sim/custom/controll/parking_brake",
2260 TYPE_INT)
2261 self._cgIndex = len(data)
2262 self._addDatarefWithIndexMember(data,
2263 "sim/custom/misc/cg_pos_actual",
2264 TYPE_FLOAT)
2265
2266 def getAircraftState(self, aircraft, timestamp, data):
2267 """Get the aircraft state.
2268
2269 Get it from the parent, and then invert the pitot heat state."""
2270 state = super(FelisT154B2Model, self).getAircraftState(aircraft,
2271 timestamp,
2272 data)
2273
2274 state.parking = data[self._parkingBrakeIndex]!=0
2275 state.cog = data[self._cgIndex]/100.0
2276
2277 return state
2278
2279#------------------------------------------------------------------------------
2280
2281class YK40Model(GenericAircraftModel):
2282 """Generic model for the Yakovlev Yak-40 aircraft."""
2283 fuelTanks = [const.FUELTANK_LEFT, const.FUELTANK_RIGHT]
2284
2285 def __init__(self):
2286 """Construct the model."""
2287 super(YK40Model, self). \
2288 __init__(flapsNotches = [0, 20, 35],
2289 fuelTanks = YK40Model.fuelTanks,
2290 numEngines = 2)
2291
2292 @property
2293 def name(self):
2294 """Get the name for this aircraft model."""
2295 return "X-Plane/Generic Yakovlev Yak-40"
2296
2297#------------------------------------------------------------------------------
2298
2299_genericModels = { const.AIRCRAFT_B736 : B737Model,
2300 const.AIRCRAFT_B737 : B737Model,
2301 const.AIRCRAFT_B738 : B737Model,
2302 const.AIRCRAFT_B738C : B737Model,
2303 const.AIRCRAFT_B732 : B737Model,
2304 const.AIRCRAFT_B733 : B737Model,
2305 const.AIRCRAFT_B734 : B737Model,
2306 const.AIRCRAFT_B735 : B737Model,
2307 const.AIRCRAFT_DH8D : DH8DModel,
2308 const.AIRCRAFT_B762 : B767Model,
2309 const.AIRCRAFT_B763 : B767Model,
2310 const.AIRCRAFT_CRJ2 : CRJ2Model,
2311 const.AIRCRAFT_F70 : F70Model,
2312 const.AIRCRAFT_DC3 : DC3Model,
2313 const.AIRCRAFT_T134 : T134Model,
2314 const.AIRCRAFT_T154 : T154Model,
2315 const.AIRCRAFT_YK40 : YK40Model }
2316
2317#------------------------------------------------------------------------------
2318
2319AircraftModel.registerSpecial(ZiboB738Model)
2320AircraftModel.registerSpecial(LevelUpB736Model)
2321AircraftModel.registerSpecial(LevelUpB737Model)
2322AircraftModel.registerSpecial(LevelUpB738Model)
2323AircraftModel.registerSpecial(FJSDH8DModel)
2324AircraftModel.registerSpecial(FJSDH8DXPModel)
2325AircraftModel.registerSpecial(FelisT154Model)
2326AircraftModel.registerSpecial(FelisT154B2Model)
2327
2328#------------------------------------------------------------------------------
2329
2330# if __name__ == "__main__":
2331# class ConnectionListener:
2332# def connected(self, fsType, descriptor):
2333# """Called when a connection has been established to the flight
2334# simulator of the given type."""
2335# print "fs.ConnectionListener.connected, fsType:", fsType, ", descriptor:", descriptor
2336
2337# def connectionFailed(self):
2338# """Called when the connection could not be established."""
2339# print "fs.ConnectionListener.connectionFailed"
2340
2341# def disconnected(self):
2342# """Called when a connection to the flight simulator has been broken."""
2343# print "fs.ConnectionListener.disconnected"
2344
2345# class Config:
2346# def __init__(self):
2347# self.onlineACARS = False
2348# self.realIASSmoothingLength = 2
2349# self.realVSSmoothingLength = 2
2350# self.enableSounds = False
2351# self.usingFS2Crew = False
2352
2353# def isMessageTypeFS(self, type):
2354# return True
2355
2356
2357
2358# class GUI:
2359# def __init__(self):
2360# self.config = Config()
2361# self.entranceExam = False
2362# self.zfw = 30000.0
2363
2364# def resetFlightStatus(self):
2365# pass
2366
2367# def setRating(self, value):
2368# pass
2369
2370# def insertFlightLogLine(self, index, ts, text, isFault):
2371# pass
2372
2373# def setStage(self, stage):
2374# pass
2375
2376
2377# from i18n import setLanguage
2378
2379# setLanguage("/home/vi/munka/repules/mlx", "en")
2380
2381# from logger import Logger
2382# from flight import Flight
2383# from acft import DH8D
2384
2385# gui = GUI()
2386
2387# logger = Logger(gui)
2388
2389# flight = Flight(logger, gui)
2390# acft = DH8D(flight)
2391
2392# Watchdog()
2393
2394# connectionListener = ConnectionListener()
2395# simulator = Simulator(connectionListener, connectAttempts = 3)
2396
2397# simulator.connect(acft)
2398
2399# time.sleep(2)
2400
2401# simulator.startMonitoring()
2402
2403# simulator.sendMessage("[MLX] Flight stage: Taxi", duration = 3)
2404
2405# time.sleep(4)
2406
2407# simulator.sendMessage("[MLX] Free gates: 1, 2, 3, 4, 5, 6, 25, 26, 27, 32, 33, 34, 35, 36, 37, 38, 39, 42, 43, 45, 107, 108, 109, R113, R114, R115, R116, R117, R210, R211, R212, R212A, R220, R221, R222, R223, R224, R225, R226, R227, R270, R271, R272, R274, R275, R276, R277, R278, R278A, R279", duration = 20)
2408# #simulator.sendMessage("[MLX] Free gates: 1, 2, 3, 4, 5, 6, 25, 26, 27, 32, 33, 34, 35, 36, 37, 38, 39, 42, 43, 45, 107, 108, 109, R113, R114, R115, R116", duration = 20)
2409# #simulator.sendMessage("[MLX] Free gates: 1, 2, 3, 4, 5, 6, 25, 26, 27, 32, 33, 34, 35, 36, 37, 38, 39, 42, 43, 45, 107, 108, 109, R113, R114, R115", duration = 20)
2410
2411# time.sleep(30)
2412
2413# simulator.sendMessage("[MLX] Hello", duration = 3)
2414
2415# time.sleep(10)
Note: See TracBrowser for help on using the repository browser.