source: src/mlx/xplane.py@ 1127:b9c7af542746

python3
Last change on this file since 1127:b9c7af542746 was 1122:84d0a094eb3f, checked in by István Váradi <ivaradi@…>, 12 months ago

Correct spoiler handling for the Felis Tu-154B2

File size: 94.2 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 FFSTSB767Model(B767Model):
1956 """Model handler for the FlighFactor and StepToSky Boeing 767 aircraft."""
1957 @staticmethod
1958 def doesHandle(aircraft, data):
1959 """Determine if this model handler handles the aircraft with the given
1960 name."""
1961 (tailnum, author, description, notes, icao, liveryPath) = data
1962 return (aircraft.type==const.AIRCRAFT_B762 and
1963 tailnum=="FFSTS" and icao=="B762") or \
1964 (aircraft.type==const.AIRCRAFT_B763 and
1965 tailnum=="FFSTS" and icao=="B763")
1966
1967 @property
1968 def name(self):
1969 """Get the name for this aircraft model."""
1970 return "X-Plane/FlightFactor and StepToSky Boeing 767"
1971
1972 def getAircraftState(self, aircraft, timestamp, data):
1973 """Get the aircraft state.
1974
1975 Get it from the parent, and then invert the pitot heat state."""
1976 state = super(FFSTSB767Model, self).getAircraftState(aircraft,
1977 timestamp,
1978 data)
1979 if state.spoilersExtension<40:
1980 state.spoilersExtension = 0.0
1981
1982 return state
1983
1984#------------------------------------------------------------------------------
1985
1986class DH8DModel(GenericAircraftModel):
1987 """Generic model for the Bombardier Dash 8-Q400 aircraft."""
1988 fuelTanks = [const.FUELTANK_LEFT, const.FUELTANK_RIGHT]
1989
1990 def __init__(self):
1991 """Construct the model."""
1992 super(DH8DModel, self). \
1993 __init__(flapsNotches = [0, 5, 10, 15, 35],
1994 fuelTanks = DH8DModel.fuelTanks,
1995 numEngines = 2)
1996
1997 @property
1998 def name(self):
1999 """Get the name for this aircraft model."""
2000 return "X-Plane/Generic Bombardier Dash 8-Q400"
2001
2002#------------------------------------------------------------------------------
2003
2004class FJSDH8DModel(DH8DModel):
2005 """Model handler for the FlyJSim Dash 8-Q400."""
2006 @staticmethod
2007 def doesHandle(aircraft, data):
2008 """Determine if this model handler handles the aircraft with the given
2009 name."""
2010 (tailnum, author, description, notes, icao, liveryPath) = data
2011 return aircraft.type==const.AIRCRAFT_DH8D and \
2012 description.find("Dash 8 Q400")!=-1 and \
2013 ((author in ["2012", "2013"] and tailnum=="N62890") or \
2014 author.find("Jack Skieczius")!=-1)
2015
2016 @property
2017 def name(self):
2018 """Get the name for this aircraft model."""
2019 return "X-Plane/FlyJSim Bombardier Dash 8-Q400"
2020
2021 def addMonitoringData(self, data, fsType):
2022 """Add the model-specific monitoring data to the given array."""
2023 super(FJSDH8DModel, self).addMonitoringData(data, fsType)
2024
2025 self._speedBrakeIndex = len(data)
2026 self._addDatarefWithIndexMember(data,
2027 "sim/flightmodel2/wing/speedbrake1_deg",
2028 (TYPE_FLOAT_ARRAY, 2))
2029 self._addDatarefWithIndexMember(data,
2030 "sim/flightmodel2/wing/speedbrake2_deg",
2031 (TYPE_FLOAT_ARRAY, 2))
2032
2033
2034 def getAircraftState(self, aircraft, timestamp, data):
2035 """Get the aircraft state.
2036
2037 Get it from the parent, and then invert the pitot heat state."""
2038 state = super(FJSDH8DModel, self).getAircraftState(aircraft,
2039 timestamp,
2040 data)
2041 state.antiCollisionLightsOn = \
2042 state.antiCollisionLightsOn or state.strobeLightsOn
2043 state.cog = (state.cog / 0.0254 + 21.504) / 94.512
2044
2045 # It seems that N1 does not always go down to 0 properly
2046 # (maybe due to winds?)
2047 state.n1 = [0 if n1<2.0 else n1 for n1 in state.n1]
2048
2049 state.spoilersExtension = \
2050 sum(data[self._speedBrakeIndex] + data[self._speedBrakeIndex+1])/4
2051
2052 return state
2053
2054#------------------------------------------------------------------------------
2055
2056class FJSDH8DXPModel(DH8DModel):
2057 """Model handler for the FlyJSim Q4XP."""
2058 @staticmethod
2059 def doesHandle(aircraft, data):
2060 """Determine if this model handler handles the aircraft with the given
2061 name."""
2062 (tailnum, author, description, notes, icao, liveryPath) = data
2063 return aircraft.type==const.AIRCRAFT_DH8D and \
2064 description.find("Dash 8 Q400")!=-1 and \
2065 author=="FlyJSim" and tailnum=="N62890"
2066
2067 @property
2068 def name(self):
2069 """Get the name for this aircraft model."""
2070 return "X-Plane/FlyJSim Q4XP"
2071
2072 def addMonitoringData(self, data, fsType):
2073 """Add the model-specific monitoring data to the given array."""
2074 super(FJSDH8DXPModel, self).addMonitoringData(data, fsType)
2075
2076 self._speedBrakeIndex = len(data)
2077 self._addDatarefWithIndexMember(data,
2078 "sim/flightmodel2/wing/spoiler1_deg",
2079 (TYPE_FLOAT_ARRAY, 32))
2080 self._addDatarefWithIndexMember(data,
2081 "sim/flightmodel2/wing/spoiler2_deg",
2082 (TYPE_FLOAT_ARRAY, 32))
2083
2084 self._gearIndex = len(data)
2085 self._addDatarefWithIndexMember(data,
2086 "FJS/Q4XP/Manips/GearDeployHandle_Ctl",
2087 (TYPE_FLOAT_ARRAY, 1))
2088
2089 self._apIndex = len(data)
2090 self._addDatarefWithIndexMember(data,
2091 "FJS/Q4XP/FMA/roll_act",
2092 TYPE_INT)
2093 self._addDatarefWithIndexMember(data,
2094 "FJS/Q4XP/FMA/pitch_act",
2095 TYPE_INT)
2096
2097 self._propPitchIndex = len(data)
2098 self._addDatarefWithIndexMember(data,
2099 "sim/flightmodel2/engines/prop_pitch_deg",
2100 (TYPE_FLOAT_ARRAY, 2))
2101
2102
2103 def getAircraftState(self, aircraft, timestamp, data):
2104 """Get the aircraft state.
2105
2106 Get it from the parent, and then invert the pitot heat state."""
2107 state = super(FJSDH8DXPModel, self).getAircraftState(aircraft,
2108 timestamp,
2109 data)
2110 state.antiCollisionLightsOn = \
2111 state.antiCollisionLightsOn or state.strobeLightsOn
2112 state.cog = (state.cog * 41.656436697 + 27.586779769)/100.0
2113
2114 # It seems that N1 does not always go down to 0 properly
2115 # (maybe due to winds?)
2116 state.n1 = [0 if n1<2.0 else n1 for n1 in state.n1]
2117
2118 state.spoilersExtension = \
2119 sum(data[self._speedBrakeIndex] + data[self._speedBrakeIndex+1])/4
2120 if state.spoilersExtension<40:
2121 state.spoilersExtension = 0.0
2122
2123 state.gearControlDown = data[self._gearIndex][0]>0.5
2124
2125 state.apHeadingHold = data[self._apIndex]==4
2126 state.apAltitudeHold = data[self._apIndex+1] in [4, 5]
2127
2128 state.reverser = [p<=-12.0 for p in data[self._propPitchIndex]]
2129
2130 return state
2131
2132#------------------------------------------------------------------------------
2133
2134class CRJ2Model(GenericAircraftModel):
2135 """Generic model for the Bombardier CRJ-200 aircraft."""
2136 fuelTanks = [const.FUELTANK_LEFT, const.FUELTANK_CENTRE, const.FUELTANK_RIGHT]
2137
2138 def __init__(self):
2139 """Construct the model."""
2140 super(CRJ2Model, self). \
2141 __init__(flapsNotches = [0, 8, 20, 30, 45],
2142 fuelTanks = CRJ2Model.fuelTanks,
2143 numEngines = 2)
2144
2145 @property
2146 def name(self):
2147 """Get the name for this aircraft model."""
2148 return "X-Plane/Generic Bombardier CRJ-200"
2149
2150#------------------------------------------------------------------------------
2151
2152class F70Model(GenericAircraftModel):
2153 """Generic model for the Fokker F70 aircraft."""
2154 fuelTanks = [const.FUELTANK_LEFT, const.FUELTANK_CENTRE, const.FUELTANK_RIGHT]
2155
2156 def __init__(self):
2157 """Construct the model."""
2158 super(F70Model, self). \
2159 __init__(flapsNotches = [0, 8, 15, 25, 42],
2160 fuelTanks = F70Model.fuelTanks,
2161 numEngines = 2)
2162
2163 @property
2164 def name(self):
2165 """Get the name for this aircraft model."""
2166 return "X-Plane/Generic Fokker 70"
2167
2168#------------------------------------------------------------------------------
2169
2170class DC3Model(GenericAircraftModel):
2171 """Generic model for the Lisunov Li-2 (DC-3) aircraft."""
2172 fuelTanks = [const.FUELTANK_LEFT, const.FUELTANK_CENTRE,
2173 const.FUELTANK_RIGHT]
2174 # fuelTanks = [const.FUELTANK_LEFT_AUX, const.FUELTANK_LEFT,
2175 # const.FUELTANK_RIGHT, const.FUELTANK_RIGHT_AUX]
2176
2177 def __init__(self):
2178 """Construct the model."""
2179 super(DC3Model, self). \
2180 __init__(flapsNotches = [0, 15, 30, 45],
2181 fuelTanks = DC3Model.fuelTanks,
2182 numEngines = 2, isN1 = False)
2183 self._leftLevel = 0.0
2184 self._rightLevel = 0.0
2185
2186 @property
2187 def name(self):
2188 """Get the name for this aircraft model."""
2189 return "X-Plane/Generic Lisunov Li-2 (DC-3)"
2190
2191#------------------------------------------------------------------------------
2192
2193class T134Model(GenericAircraftModel):
2194 """Generic model for the Tupolev Tu-134 aircraft."""
2195 fuelTanks = [const.FUELTANK_LEFT_TIP, const.FUELTANK_EXTERNAL1,
2196 const.FUELTANK_LEFT_AUX,
2197 const.FUELTANK_CENTRE,
2198 const.FUELTANK_RIGHT_AUX,
2199 const.FUELTANK_EXTERNAL2, const.FUELTANK_RIGHT_TIP]
2200
2201 def __init__(self):
2202 """Construct the model."""
2203 super(T134Model, self). \
2204 __init__(flapsNotches = [0, 10, 20, 30],
2205 fuelTanks = T134Model.fuelTanks,
2206 numEngines = 2)
2207
2208 @property
2209 def name(self):
2210 """Get the name for this aircraft model."""
2211 return "X-Plane/Generic Tupolev Tu-134"
2212
2213#------------------------------------------------------------------------------
2214
2215class T154Model(GenericAircraftModel):
2216 """Generic model for the Tupolev Tu-154 aircraft."""
2217 fuelTanks = [const.FUELTANK_CENTRE, const.FUELTANK_CENTRE2,
2218 const.FUELTANK_RIGHT, const.FUELTANK_LEFT,
2219 const.FUELTANK_RIGHT_AUX, const.FUELTANK_LEFT_AUX]
2220
2221 def __init__(self):
2222 """Construct the model."""
2223 super(T154Model, self). \
2224 __init__(flapsNotches = [0, 15, 28, 45],
2225 fuelTanks = T154Model.fuelTanks,
2226 numEngines = 3)
2227
2228 @property
2229 def name(self):
2230 """Get the name for this aircraft model."""
2231 return "X-Plane/Generic Tupolev Tu-154"
2232
2233 def getAircraftState(self, aircraft, timestamp, data):
2234 """Get an aircraft state object for the given monitoring data.
2235
2236 This removes the reverser value for the middle engine."""
2237 state = super(T154Model, self).getAircraftState(aircraft, timestamp, data)
2238 del state.reverser[1]
2239 return state
2240
2241#------------------------------------------------------------------------------
2242
2243class FelisT154Model(T154Model):
2244 """Model for Felis' Tupolev Tu-154-M aircraft."""
2245 @staticmethod
2246 def doesHandle(aircraft, data):
2247 """Determine if this model handler handles the aircraft with the given
2248 name."""
2249 (tailnum, author, description, notes, icao, liveryPath) = data
2250 return aircraft.type==const.AIRCRAFT_T154 and \
2251 author.find("Felis")!=-1 and \
2252 description.find("Tu154M")!=-1
2253
2254 def __init__(self):
2255 """Construct the model."""
2256 super(T154Model, self). \
2257 __init__(flapsNotches = [0, 15, 28, 36, 45],
2258 fuelTanks = T154Model.fuelTanks,
2259 numEngines = 3)
2260
2261 @property
2262 def name(self):
2263 """Get the name for this aircraft model."""
2264 return "X-Plane/Felis Tupolev Tu-154-M"
2265
2266#------------------------------------------------------------------------------
2267
2268class FelisT154B2Model(T154Model):
2269 """Model for Felis' Tupolev Tu-154-B2 aircraft."""
2270 @staticmethod
2271 def doesHandle(aircraft, data):
2272 """Determine if this model handler handles the aircraft with the given
2273 name."""
2274 (tailnum, author, description, notes, icao, liveryPath) = data
2275 return aircraft.type==const.AIRCRAFT_T154 and \
2276 author.find("Felis")!=-1 and \
2277 description.find("Tu154B2")!=-1
2278
2279 @property
2280 def name(self):
2281 """Get the name for this aircraft model."""
2282 return "X-Plane/Felis Tupolev Tu-154-B2"
2283
2284 def addMonitoringData(self, data, fsType):
2285 """Add the model-specific monitoring data to the given array."""
2286 super(FelisT154B2Model, self).addMonitoringData(data, fsType)
2287
2288 self._parkingBrakeIndex = len(data)
2289 self._addDatarefWithIndexMember(data,
2290 "sim/custom/controll/parking_brake",
2291 TYPE_INT)
2292 self._cgIndex = len(data)
2293 self._addDatarefWithIndexMember(data,
2294 "sim/custom/misc/cg_pos_actual",
2295 TYPE_FLOAT)
2296
2297 self._flapsControlIndex = len(data)
2298 self._addDatarefWithIndexMember(data,
2299 "sim/custom/controll/flaps_lever",
2300 TYPE_FLOAT)
2301 self._flapsIndex = len(data)
2302 self._addDatarefWithIndexMember(data,
2303 "sim/flightmodel/controls/fla1_def",
2304 (TYPE_FLOAT_ARRAY, 2, 8))
2305
2306 self._spoilersIndex = len(data)
2307 self._addDatarefWithIndexMember(data,
2308 "sim/flightmodel2/wing/spoiler1_deg",
2309 (TYPE_FLOAT_ARRAY, 2, 0))
2310 self._addDatarefWithIndexMember(data,
2311 "sim/flightmodel2/wing/spoiler2_deg",
2312 (TYPE_FLOAT_ARRAY, 2, 2))
2313
2314 def getAircraftState(self, aircraft, timestamp, data):
2315 """Get the aircraft state.
2316
2317 Get it from the parent, and then invert the pitot heat state."""
2318 state = super(FelisT154B2Model, self).getAircraftState(aircraft,
2319 timestamp,
2320 data)
2321
2322 state.parking = data[self._parkingBrakeIndex]!=0
2323 state.cog = data[self._cgIndex]/100.0
2324 state.flapsSet = data[self._flapsControlIndex]
2325 state.flaps = data[self._flapsIndex][0]
2326
2327 state.spoilersExtension = max(
2328 max(data[self._spoilersIndex])*100.0/50.0,
2329 max(data[self._spoilersIndex+1])*100.0/45.0)
2330 if state.spoilersExtension<=10.0:
2331 state.spoilersExtension = 0.0
2332
2333 return state
2334
2335#------------------------------------------------------------------------------
2336
2337class YK40Model(GenericAircraftModel):
2338 """Generic model for the Yakovlev Yak-40 aircraft."""
2339 fuelTanks = [const.FUELTANK_LEFT, const.FUELTANK_RIGHT]
2340
2341 def __init__(self):
2342 """Construct the model."""
2343 super(YK40Model, self). \
2344 __init__(flapsNotches = [0, 20, 35],
2345 fuelTanks = YK40Model.fuelTanks,
2346 numEngines = 2)
2347
2348 @property
2349 def name(self):
2350 """Get the name for this aircraft model."""
2351 return "X-Plane/Generic Yakovlev Yak-40"
2352
2353#------------------------------------------------------------------------------
2354
2355_genericModels = { const.AIRCRAFT_B736 : B737Model,
2356 const.AIRCRAFT_B737 : B737Model,
2357 const.AIRCRAFT_B738 : B737Model,
2358 const.AIRCRAFT_B738C : B737Model,
2359 const.AIRCRAFT_B732 : B737Model,
2360 const.AIRCRAFT_B733 : B737Model,
2361 const.AIRCRAFT_B734 : B737Model,
2362 const.AIRCRAFT_B735 : B737Model,
2363 const.AIRCRAFT_DH8D : DH8DModel,
2364 const.AIRCRAFT_B762 : B767Model,
2365 const.AIRCRAFT_B763 : B767Model,
2366 const.AIRCRAFT_CRJ2 : CRJ2Model,
2367 const.AIRCRAFT_F70 : F70Model,
2368 const.AIRCRAFT_DC3 : DC3Model,
2369 const.AIRCRAFT_T134 : T134Model,
2370 const.AIRCRAFT_T154 : T154Model,
2371 const.AIRCRAFT_YK40 : YK40Model }
2372
2373#------------------------------------------------------------------------------
2374
2375AircraftModel.registerSpecial(ZiboB738Model)
2376AircraftModel.registerSpecial(LevelUpB736Model)
2377AircraftModel.registerSpecial(LevelUpB737Model)
2378AircraftModel.registerSpecial(LevelUpB738Model)
2379AircraftModel.registerSpecial(FFSTSB767Model)
2380AircraftModel.registerSpecial(FJSDH8DModel)
2381AircraftModel.registerSpecial(FJSDH8DXPModel)
2382AircraftModel.registerSpecial(FelisT154Model)
2383AircraftModel.registerSpecial(FelisT154B2Model)
2384
2385#------------------------------------------------------------------------------
2386
2387# if __name__ == "__main__":
2388# class ConnectionListener:
2389# def connected(self, fsType, descriptor):
2390# """Called when a connection has been established to the flight
2391# simulator of the given type."""
2392# print "fs.ConnectionListener.connected, fsType:", fsType, ", descriptor:", descriptor
2393
2394# def connectionFailed(self):
2395# """Called when the connection could not be established."""
2396# print "fs.ConnectionListener.connectionFailed"
2397
2398# def disconnected(self):
2399# """Called when a connection to the flight simulator has been broken."""
2400# print "fs.ConnectionListener.disconnected"
2401
2402# class Config:
2403# def __init__(self):
2404# self.onlineACARS = False
2405# self.realIASSmoothingLength = 2
2406# self.realVSSmoothingLength = 2
2407# self.enableSounds = False
2408# self.usingFS2Crew = False
2409
2410# def isMessageTypeFS(self, type):
2411# return True
2412
2413
2414
2415# class GUI:
2416# def __init__(self):
2417# self.config = Config()
2418# self.entranceExam = False
2419# self.zfw = 30000.0
2420
2421# def resetFlightStatus(self):
2422# pass
2423
2424# def setRating(self, value):
2425# pass
2426
2427# def insertFlightLogLine(self, index, ts, text, isFault):
2428# pass
2429
2430# def setStage(self, stage):
2431# pass
2432
2433
2434# from i18n import setLanguage
2435
2436# setLanguage("/home/vi/munka/repules/mlx", "en")
2437
2438# from logger import Logger
2439# from flight import Flight
2440# from acft import DH8D
2441
2442# gui = GUI()
2443
2444# logger = Logger(gui)
2445
2446# flight = Flight(logger, gui)
2447# acft = DH8D(flight)
2448
2449# Watchdog()
2450
2451# connectionListener = ConnectionListener()
2452# simulator = Simulator(connectionListener, connectAttempts = 3)
2453
2454# simulator.connect(acft)
2455
2456# time.sleep(2)
2457
2458# simulator.startMonitoring()
2459
2460# simulator.sendMessage("[MLX] Flight stage: Taxi", duration = 3)
2461
2462# time.sleep(4)
2463
2464# 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)
2465# #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)
2466# #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)
2467
2468# time.sleep(30)
2469
2470# simulator.sendMessage("[MLX] Hello", duration = 3)
2471
2472# time.sleep(10)
Note: See TracBrowser for help on using the repository browser.