source: src/mlx/xplane.py@ 1092:f3c535a548c2

python3
Last change on this file since 1092:f3c535a548c2 was 1073:a5a22d24f890, checked in by István Váradi <ivaradi@…>, 23 months ago

New constant for X-Plane 12

File size: 90.0 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:
967 diff = self._lastZuluSeconds - zuluSeconds
968 print("xplane.Simulator._getTimestamp: Zulu seconds have gone backwards: %f -> %f, diff: %f" % \
969 (self._lastZuluSeconds, zuluSeconds, diff))
970 if diff>23*60*60:
971 self._timestampDaysOffset += 1
972 else:
973 zuluSeconds = self._lastZuluSeconds
974
975 self._lastZuluSeconds = zuluSeconds
976
977 timestamp = self._timestampBase
978 timestamp += self._timestampDaysOffset * 24 * 3600
979 timestamp += zuluSeconds
980
981 return timestamp
982
983 def _startDefaultNormal(self):
984 """Start the default normal periodic request."""
985 assert self._normalRequestID is None
986 self._timestampBase = None
987 self._normalRequestID = \
988 self._handler.requestPeriodicRead(1.0,
989 Simulator.normalData,
990 self._handleNormal)
991
992 def _stopNormal(self):
993 """Stop the normal period request."""
994 assert self._normalRequestID is not None
995 self._handler.clearPeriodic(self._normalRequestID)
996 self._normalRequestID = None
997 self._monitoring = False
998
999 def _handleNormal(self, data, extra):
1000 """Handle the reply to the normal request.
1001
1002 At the beginning the result consists the data for normalData. When
1003 monitoring is started, it contains the result also for the
1004 aircraft-specific values.
1005 """
1006 timestamp = self._getTimestamp(data)
1007
1008 createdNewModel = self._setAircraftName(timestamp,
1009 data.getString(2),
1010 data.getString(3),
1011 data.getString(4),
1012 data.getString(5),
1013 data.getString(6),
1014 data.getString(7))
1015 if self._fuelCallback is not None:
1016 self._aircraftModel.getFuel(self._handler, self._fuelCallback)
1017 self._fuelCallback = None
1018
1019 if self._monitoringRequested and not self._monitoring:
1020 self._stopNormal()
1021 self._startMonitoring()
1022 elif self._monitoring and not self._monitoringRequested:
1023 self._stopNormal()
1024 self._startDefaultNormal()
1025 elif self._monitoring and self._aircraftModel is not None and \
1026 not createdNewModel:
1027 aircraftState = self._aircraftModel.getAircraftState(self._aircraft,
1028 timestamp, data)
1029
1030 self._aircraft.handleState(aircraftState)
1031
1032 def _setAircraftName(self, timestamp, tailnum, author, description,
1033 notes, icao, liveryPath):
1034 """Set the name of the aicraft and if it is different from the
1035 previous, create a new model for it.
1036
1037 If so, also notifty the aircraft about the change.
1038
1039 Return if a new model was created."""
1040 aircraftInfo = (tailnum, author, description, notes, icao, liveryPath)
1041 if aircraftInfo==self._aircraftInfo:
1042 return False
1043
1044 print("xplane.Simulator: new data: %s, %s, %s, %s, %s, %s" % \
1045 (tailnum, author, description, notes, icao, liveryPath))
1046
1047 self._aircraftInfo = aircraftInfo
1048 needNew = self._aircraftModel is None
1049 needNew = needNew or\
1050 not self._aircraftModel.doesHandle(self._aircraft, aircraftInfo)
1051 if not needNew:
1052 specialModel = AircraftModel.findSpecial(self._aircraft,
1053 aircraftInfo)
1054 needNew = specialModel is not None and \
1055 specialModel is not self._aircraftModel.__class__
1056
1057 if needNew:
1058 self._setAircraftModel(AircraftModel.create(self._aircraft,
1059 aircraftInfo))
1060
1061 self._aircraft.modelChanged(timestamp, description,
1062 self._aircraftModel.name)
1063
1064 return needNew
1065
1066 def _setAircraftModel(self, model):
1067 """Set a new aircraft model.
1068
1069 It will be queried for the data to monitor and the monitoring request
1070 will be replaced by a new one."""
1071 self._aircraftModel = model
1072 model.simulator = self
1073
1074 if self._monitoring:
1075 self._stopNormal()
1076 self._startMonitoring()
1077
1078 def _startMonitoring(self):
1079 """Start monitoring with the current aircraft model."""
1080 data = Simulator.normalData[:]
1081 self._aircraftModel.addMonitoringData(data, self._fsType)
1082
1083 self._normalRequestID = \
1084 self._handler.requestPeriodicRead(1.0, data,
1085 self._handleNormal)
1086 self._monitoring = True
1087
1088 def _addFlareRate(self, data):
1089 """Append a flare rate to the list of last rates."""
1090 if len(self._flareRates)>=3:
1091 del self._flareRates[0]
1092 self._flareRates.append(data)
1093
1094 def _handleFlare1(self, data, normal):
1095 """Handle the first stage of flare monitoring."""
1096 #self._aircraft.logger.debug("handleFlare1: " + str(data))
1097 if data[1]<=50.0*0.3048:
1098 self._flareStart = time.time()
1099 self._flareStartFS = data[0]
1100 self._handler.clearPeriodic(self._flareRequestID)
1101 self._handler.requestRead(Simulator.flareStartData,
1102 self._handleFlareStart)
1103
1104 self._addFlareRate(data[2])
1105
1106 def _handleFlareStart(self, data, extra):
1107 """Handle the data need to notify the aircraft about the starting of
1108 the flare."""
1109 #self._aircraft.logger.debug("handleFlareStart: " + str(data))
1110 if data is not None:
1111 windDirection = data[1]
1112 if windDirection<0.0: windDirection += 360.0
1113 self._aircraft.flareStarted(data[0], windDirection, data[2],
1114 self._flareStart, self._flareStartFS)
1115
1116 self._flareRequestID = \
1117 self._handler.requestPeriodicRead(0.1,
1118 Simulator.flareData2,
1119 self._handleFlare2)
1120
1121 def _handleFlare2(self, data, normal):
1122 """Handle the first stage of flare monitoring."""
1123 #self._aircraft.logger.debug("handleFlare2: " + str(data))
1124 if data[1]!=0:
1125 flareEnd = time.time()
1126 self._handler.clearPeriodic(self._flareRequestID)
1127 self._flareRequestID = None
1128
1129 flareEndFS = data[0]
1130 if flareEndFS<self._flareStartFS:
1131 flareEndFS += 86400.0
1132
1133 tdRate = min(self._flareRates)
1134 tdRateCalculatedByFS = False
1135
1136 heading = data[6]
1137 if heading<0.0: heading += 360.0
1138
1139 self._aircraft.flareFinished(flareEnd, flareEndFS,
1140 tdRate, tdRateCalculatedByFS,
1141 data[3], data[4], data[5], heading)
1142 else:
1143 self._addFlareRate(data[2])
1144
1145 def _handleZFW(self, data, callback):
1146 """Callback for a ZFW retrieval request."""
1147 zfw = data[0] + data[1]
1148 callback(zfw)
1149
1150 def _handleTime(self, data, callback):
1151 """Callback for a time retrieval request."""
1152 callback(self._getTimestamp(data))
1153
1154 def _handleWeights(self, data, callback):
1155 """Callback for the weights retrieval request."""
1156 dow = data[0]
1157 payload = data[1]
1158 zfw = dow + payload
1159 grossWeight = data[2]
1160 callback(dow, payload, zfw, grossWeight)
1161
1162 def _handleMessageSent(self, success, disconnect):
1163 """Callback for a message sending request."""
1164 #print "xplra.Simulator._handleMessageSent", disconnect
1165 if disconnect:
1166 self._handler.disconnect()
1167
1168 def _handleHotkeysRegistered(self, success, hotkeySet):
1169 """Handle the result of the hotkeys having been written."""
1170 (id, generation) = hotkeySet
1171 with self._hotkeyLock:
1172 if success and id==self._hotkeySetID and \
1173 generation==self._hotkeySetGeneration:
1174 self._hotkeyRequestID = \
1175 self._handler.requestHotkeysState(0.5,
1176 self._handleHotkeys,
1177 hotkeySet)
1178
1179 def _handleHotkeys(self, data, hotkeySet):
1180 """Handle the hotkeys."""
1181 (id, generation) = hotkeySet
1182 with self._hotkeyLock:
1183 if id!=self._hotkeySetID or generation!=self._hotkeySetGeneration:
1184 return
1185
1186 callback = self._hotkeyCallback
1187 offsets = self._hotkeyOffets
1188
1189 hotkeysPressed = []
1190 for i in range(0, len(data)):
1191 if data[i]:
1192 hotkeysPressed.append(i)
1193
1194 if hotkeysPressed:
1195 callback(id, hotkeysPressed)
1196
1197 def _clearHotkeyRequest(self):
1198 """Clear the hotkey request in the handler if there is any."""
1199 if self._hotkeyRequestID is not None:
1200 self._handler.unregisterHotkeys(self._hotkeysUnregistered)
1201 self._handler.clearPeriodic(self._hotkeyRequestID)
1202 self._hotkeyRequestID = None
1203
1204 def _hotkeysUnregistered(self, result, extra):
1205 """Called when the hotkeys have been unregistered."""
1206 pass
1207
1208#------------------------------------------------------------------------------
1209
1210class AircraftModel(object):
1211 """Base class for the aircraft models.
1212
1213 Aircraft models handle the data arriving from X-Plane and turn it into an
1214 object describing the aircraft's state."""
1215 monitoringData = [ ("paused",
1216 "sim/time/paused", TYPE_INT),
1217 ("latitude",
1218 "sim/flightmodel/position/latitude", TYPE_DOUBLE),
1219 ("longitude",
1220 "sim/flightmodel/position/longitude", TYPE_DOUBLE),
1221 ("replay",
1222 "sim/operation/prefs/replay_mode", TYPE_INT),
1223 ("overspeed",
1224 "sim/flightmodel/failures/over_vne", TYPE_INT),
1225 ("stalled",
1226 "sim/flightmodel/failures/stallwarning", TYPE_INT),
1227 ("onTheGround",
1228 "sim/flightmodel/failures/onground_any", TYPE_INT),
1229 ("emptyWeight",
1230 "sim/aircraft/weight/acf_m_empty", TYPE_FLOAT),
1231 ("payloadWeight",
1232 "sim/flightmodel/weight/m_fixed", TYPE_FLOAT),
1233 ("grossWeight",
1234 "sim/flightmodel/weight/m_total", TYPE_FLOAT),
1235 ("heading",
1236 "sim/flightmodel/position/psi", TYPE_FLOAT),
1237 ("pitch",
1238 "sim/flightmodel/position/theta", TYPE_FLOAT),
1239 ("bank",
1240 "sim/flightmodel/position/phi", TYPE_FLOAT),
1241 ("ias",
1242 "sim/flightmodel/position/indicated_airspeed2",
1243 TYPE_FLOAT),
1244 ("mach",
1245 "sim/flightmodel/misc/machno", TYPE_FLOAT),
1246 ("groundSpeed",
1247 "sim/flightmodel/position/groundspeed", TYPE_FLOAT),
1248 ("vs",
1249 "sim/flightmodel/position/vh_ind_fpm2", TYPE_FLOAT),
1250 ("radioAltitude",
1251 "sim/flightmodel/position/y_agl", TYPE_FLOAT),
1252 ("altitude",
1253 "sim/flightmodel/position/elevation", TYPE_FLOAT),
1254 ("gLoad",
1255 "sim/flightmodel/forces/g_nrml", TYPE_FLOAT),
1256 ("flapsControl",
1257 "sim/flightmodel/controls/flaprqst", TYPE_FLOAT),
1258 ("flapsLeft",
1259 "sim/flightmodel/controls/flaprat", TYPE_FLOAT),
1260 ("flapsRight",
1261 "sim/flightmodel/controls/flap2rat", TYPE_FLOAT),
1262 ("navLights",
1263 "sim/cockpit/electrical/nav_lights_on", TYPE_INT),
1264 ("beaconLights",
1265 "sim/cockpit/electrical/beacon_lights_on", TYPE_INT),
1266 ("strobeLights",
1267 "sim/cockpit/electrical/strobe_lights_on", TYPE_INT),
1268 ("landingLights",
1269 "sim/cockpit/electrical/landing_lights_on", TYPE_INT),
1270 ("pitot",
1271 "sim/cockpit/switches/pitot_heat_on", TYPE_INT),
1272 ("parking",
1273 "sim/flightmodel/controls/parkbrake", TYPE_FLOAT),
1274 ("gearControl",
1275 "sim/cockpit2/controls/gear_handle_down", TYPE_INT),
1276 ("noseGear",
1277 "sim/flightmodel2/gear/deploy_ratio",
1278 (TYPE_FLOAT_ARRAY, 1)),
1279 ("spoilers",
1280 "sim/flightmodel/controls/lsplrdef", TYPE_FLOAT),
1281 ("altimeter",
1282 "sim/cockpit/misc/barometer_setting", TYPE_FLOAT),
1283 ("qnh",
1284 "sim/physics/earth_pressure_p", TYPE_FLOAT),
1285 ("nav1",
1286 "sim/cockpit/radios/nav1_freq_hz", TYPE_INT),
1287 ("nav1_obs",
1288 "sim/cockpit/radios/nav1_obs_degm", TYPE_FLOAT),
1289 ("nav2",
1290 "sim/cockpit/radios/nav2_freq_hz", TYPE_INT),
1291 ("nav2_obs",
1292 "sim/cockpit/radios/nav2_obs_degm", TYPE_FLOAT),
1293 ("adf1",
1294 "sim/cockpit/radios/adf1_freq_hz", TYPE_INT),
1295 ("adf2",
1296 "sim/cockpit/radios/adf2_freq_hz", TYPE_INT),
1297 ("squawk",
1298 "sim/cockpit/radios/transponder_code", TYPE_INT),
1299 ("windSpeed",
1300 "sim/weather/wind_speed_kt[0]", TYPE_FLOAT),
1301 ("windDirection",
1302 "sim/weather/wind_direction_degt[0]", TYPE_FLOAT),
1303 ("visibility",
1304 "sim/weather/visibility_reported_m", TYPE_FLOAT),
1305 ("cog",
1306 "sim/flightmodel/misc/cgz_ref_to_default", TYPE_FLOAT),
1307 ("xpdrC",
1308 "sim/cockpit/radios/transponder_mode", TYPE_INT),
1309 ("apMaster",
1310 "sim/cockpit/autopilot/autopilot_mode", TYPE_INT),
1311 ("apState",
1312 "sim/cockpit/autopilot/autopilot_state", TYPE_INT),
1313 ("apHeading",
1314 "sim/cockpit/autopilot/heading_mag", TYPE_FLOAT),
1315 ("apAltitude",
1316 "sim/cockpit/autopilot/altitude", TYPE_FLOAT),
1317 ("elevatorTrim",
1318 "sim/flightmodel/controls/elv_trim", TYPE_FLOAT),
1319 ("antiIceOn",
1320 "sim/cockpit/switches/anti_ice_on", TYPE_INT),
1321 ("surfaceHeat",
1322 "sim/cockpit/switches/anti_ice_surf_heat", TYPE_INT),
1323 ("propHeat",
1324 "sim/cockpit/switches/anti_ice_prop_heat", TYPE_INT),
1325 ("autopilotOn",
1326 "sim/cockpit2/autopilot/autopilot_on", TYPE_INT),
1327 ("apHeadingMode",
1328 "sim/cockpit2/autopilot/heading_mode", TYPE_INT)]
1329
1330
1331 specialModels = []
1332
1333 @staticmethod
1334 def registerSpecial(clazz):
1335 """Register the given class as a special model."""
1336 AircraftModel.specialModels.append(clazz)
1337
1338 @staticmethod
1339 def findSpecial(aircraft, aircraftInfo):
1340 for specialModel in AircraftModel.specialModels:
1341 if specialModel.doesHandle(aircraft, aircraftInfo):
1342 return specialModel
1343 return None
1344
1345 @staticmethod
1346 def create(aircraft, aircraftInfo):
1347 """Create the model for the given aircraft name, and notify the
1348 aircraft about it."""
1349 specialModel = AircraftModel.findSpecial(aircraft, aircraftInfo)
1350 if specialModel is not None:
1351 return specialModel()
1352 if aircraft.type in _genericModels:
1353 return _genericModels[aircraft.type]()
1354 else:
1355 return GenericModel()
1356
1357 @staticmethod
1358 def _convertFrequency(value):
1359 """Convert the given frequency value into a string."""
1360 return "%.2f" % (value/100.0,)
1361
1362 @staticmethod
1363 def _convertOBS(value):
1364 """Convert the given OBS value into an integer."""
1365 while value<0.0:
1366 value += 360.0
1367 return int(round(value))
1368
1369 def __init__(self, flapsNotches):
1370 """Construct the aircraft model.
1371
1372 flapsNotches is a list of degrees of flaps that are available on the aircraft."""
1373 self._flapsNotches = flapsNotches
1374 self._simulator = None
1375
1376 @property
1377 def name(self):
1378 """Get the name for this aircraft model."""
1379 return "X-Plane/Generic"
1380
1381 @property
1382 def simulator(self):
1383 """Get the simulator this aircraft model works for."""
1384 return self._simulator
1385
1386 @simulator.setter
1387 def simulator(self, simulator):
1388 """Get the simulator this aircraft model works for."""
1389 self._simulator = simulator
1390
1391 def doesHandle(self, aircraft, aircraftInfo):
1392 """Determine if the model handles the given aircraft name.
1393
1394 This default implementation returns False."""
1395 return False
1396
1397 def _addDatarefWithIndexMember(self, dest, name, type, attrName = None):
1398 """Add the given X-Plane dataref name and type to the given array and a
1399 member attribute with the given name."""
1400 dest.append((name, type))
1401 if attrName is not None:
1402 setattr(self, attrName, len(dest)-1)
1403
1404 def _addDataWithIndexMembers(self, dest, prefix, data):
1405 """Add X-Plane dataref data to the given array and also corresponding
1406 index member variables with the given prefix.
1407
1408 data is a list of triplets of the following items:
1409 - the name of the data item. The index member variable will have a name
1410 created by prepending the given prefix to this name.
1411 - the X-Plane dataref name
1412 - the dataref type
1413
1414 The latter two items will be appended to dest."""
1415 for (name, datarefName, type) in data:
1416 self._addDatarefWithIndexMember(dest, datarefName, type,
1417 prefix + name)
1418
1419 def addMonitoringData(self, data, fsType):
1420 """Add the model-specific monitoring data to the given array."""
1421 self._addDataWithIndexMembers(data, "_monidx_",
1422 AircraftModel.monitoringData)
1423
1424 def getAircraftState(self, aircraft, timestamp, data):
1425 """Get an aircraft state object for the given monitoring data."""
1426 state = fs.AircraftState()
1427
1428 lnavOn = data[self._monidx_autopilotOn]!=0 and \
1429 data[self._monidx_apHeadingMode]==2
1430
1431 state.timestamp = timestamp
1432
1433 state.latitude = data[self._monidx_latitude]
1434 state.longitude = data[self._monidx_longitude]
1435 if state.longitude>180.0: state.longitude = 360.0 - state.longitude
1436
1437 state.paused = data[self._monidx_paused]!=0 or \
1438 data[self._monidx_replay]!=0
1439 state.trickMode = data[self._monidx_replay]!=0
1440
1441 state.overspeed = data[self._monidx_overspeed]!=0
1442 state.stalled = data[self._monidx_stalled]!=0
1443 state.onTheGround = data[self._monidx_onTheGround]!=0
1444
1445 state.zfw = data[self._monidx_emptyWeight] + \
1446 data[self._monidx_payloadWeight]
1447 state.grossWeight = data[self._monidx_grossWeight]
1448
1449 state.heading = data[self._monidx_heading]
1450
1451 state.pitch = -1.0 * data[self._monidx_pitch]
1452 state.bank = data[self._monidx_bank]
1453
1454 state.ias = data[self._monidx_ias]
1455 state.mach = data[self._monidx_mach]
1456 state.groundSpeed = data[self._monidx_groundSpeed] * _mps2knots
1457 state.vs = data[self._monidx_vs]
1458
1459 state.radioAltitude = data[self._monidx_radioAltitude]/.3048
1460 state.altitude = data[self._monidx_altitude]/.3048
1461
1462 state.gLoad = data[self._monidx_gLoad]
1463
1464 flapsControl = data[self._monidx_flapsControl]
1465 flapsIndex = int(round(flapsControl * (len(self._flapsNotches)-1)))
1466 state.flapsSet = 0 if flapsIndex<1 else self._flapsNotches[flapsIndex]
1467
1468 state.flaps = self._flapsNotches[-1]*data[self._monidx_flapsLeft]
1469
1470 state.navLightsOn = data[self._monidx_navLights] != 0
1471 state.antiCollisionLightsOn = data[self._monidx_beaconLights] != 0
1472 state.landingLightsOn = data[self._monidx_landingLights] != 0
1473 state.strobeLightsOn = data[self._monidx_strobeLights] != 0
1474
1475 state.pitotHeatOn = data[self._monidx_pitot]!=0
1476
1477 state.parking = data[self._monidx_parking]>=0.5
1478
1479 state.gearControlDown = data[self._monidx_gearControl]!=0
1480 state.gearsDown = data[self._monidx_noseGear][0]>0.99
1481
1482 state.spoilersArmed = None
1483
1484 state.spoilersExtension = data[self._monidx_spoilers]*100.0
1485
1486 state.altimeter = data[self._monidx_altimeter]* _hgin2hpa
1487 state.altimeterReliable = True
1488 state.qnh = data[self._monidx_qnh]/100.0
1489
1490 state.ils = None
1491 state.ils_obs = None
1492 state.ils_manual = False
1493 state.nav1 = self._convertFrequency(data[self._monidx_nav1])
1494 state.nav1_obs = self._convertOBS(data[self._monidx_nav1_obs])
1495 state.nav1_manual = True
1496 state.nav2 = self._convertFrequency(data[self._monidx_nav2])
1497 state.nav2_obs = self._convertOBS(data[self._monidx_nav2_obs])
1498 state.nav2_manual = not lnavOn
1499 state.adf1 = str(data[self._monidx_adf1])
1500 state.adf2 = str(data[self._monidx_adf2])
1501
1502 state.squawk = "%04d" % (data[self._monidx_squawk],)
1503
1504 state.windSpeed = data[self._monidx_windSpeed]
1505 state.windDirection = data[self._monidx_windDirection]
1506 if state.windDirection<0.0: state.windDirection += 360.0
1507
1508 state.visibility = data[self._monidx_visibility]
1509
1510 state.cog = data[self._monidx_cog]
1511
1512 state.xpdrC = data[self._monidx_xpdrC]>=2
1513 state.autoXPDR = False
1514
1515 state.apMaster = data[self._monidx_apMaster]==2
1516 apState = data[self._monidx_apState]
1517 if lnavOn:
1518 state.apHeadingHold = None
1519 state.apHeading = None
1520 else:
1521 state.apHeadingHold = (apState&0x00002)!=0
1522 state.apHeading = data[self._monidx_apHeading]
1523
1524 state.apAltitudeHold = (apState&0x04000)!=0
1525 state.apAltitude = data[self._monidx_apAltitude]
1526
1527 state.elevatorTrim = data[self._monidx_elevatorTrim] * 180.0 / math.pi
1528
1529 state.antiIceOn = data[self._monidx_antiIceOn]!=0 or \
1530 data[self._monidx_surfaceHeat]!=0 or \
1531 data[self._monidx_propHeat]!=0
1532
1533 return state
1534
1535#------------------------------------------------------------------------------
1536
1537class GenericAircraftModel(AircraftModel):
1538 """A generic aircraft model that can handle the fuel levels, the N1 or RPM
1539 values and some other common parameters in a generic way."""
1540
1541 def __init__(self, flapsNotches, fuelTanks, numEngines, isN1 = True):
1542 """Construct the generic aircraft model with the given data.
1543
1544 flapsNotches is an array of how much degrees the individual flaps
1545 notches mean.
1546
1547 fuelTanks is an array of const.FUELTANK_XXX constants about the
1548 aircraft's fuel tanks. They will be converted to offsets.
1549
1550 numEngines is the number of engines the aircraft has.
1551
1552 isN1 determines if the engines have an N1 value or an RPM value
1553 (e.g. pistons)."""
1554 super(GenericAircraftModel, self).__init__(flapsNotches = flapsNotches)
1555
1556 self._fuelTanks = fuelTanks
1557 self._fuelTankCapacities = [1.0] * len(fuelTanks)
1558 self._fuelIndex = None
1559 self._numEngines = numEngines
1560 self._engineStartIndex = None
1561 self._isN1 = isN1
1562
1563 def doesHandle(self, aircraft, aircraftInfo):
1564 """Determine if the model handles the given aircraft name.
1565
1566 This implementation returns True."""
1567 return True
1568
1569 def addMonitoringData(self, data, fsType):
1570 """Add the model-specific monitoring data to the given array."""
1571 super(GenericAircraftModel, self).addMonitoringData(data, fsType)
1572
1573 self._fuelIndex = self._addFuelData(data)
1574
1575 self._engineStartIndex = len(data)
1576 if self._isN1:
1577 self._addDatarefWithIndexMember(data,
1578 "sim/flightmodel/engine/ENGN_N1_",
1579 (TYPE_FLOAT_ARRAY,
1580 self._numEngines))
1581 else:
1582 self._addDatarefWithIndexMember(data,
1583 "sim/flightmodel/engine/POINT_tacrad",
1584 (TYPE_FLOAT_ARRAY,
1585 self._numEngines))
1586
1587 self._addDatarefWithIndexMember(data,
1588 "sim/flightmodel/engine/ENGN_propmode",
1589 (TYPE_INT_ARRAY, self._numEngines))
1590
1591 def getAircraftState(self, aircraft, timestamp, data):
1592 """Get the aircraft state.
1593
1594 Get it from the parent, and then add the data about the fuel levels and
1595 the engine parameters."""
1596 state = super(GenericAircraftModel, self).getAircraftState(aircraft,
1597 timestamp,
1598 data)
1599
1600 state.fuel = []
1601 state.totalFuel = 0.0
1602
1603 fuelAmounts = data[self._fuelIndex]
1604 for i in range(0, len(self._fuelTanks)):
1605 amount = fuelAmounts[i]
1606 state.fuel.append((self._fuelTanks[i], amount))
1607 state.totalFuel += amount
1608
1609 power = data[self._engineStartIndex]
1610
1611 state.n1 = power[:] if self._isN1 else None
1612 state.rpm = None if self._isN1 else power[:]
1613
1614 propMode = data[self._engineStartIndex+1]
1615 state.reverser = [mode == 3 for mode in propMode]
1616
1617 return state
1618
1619 def getFuel(self, handler, callback):
1620 """Get the fuel information for this model.
1621
1622 See Simulator.getFuel for more information. This
1623 implementation simply queries the fuel tanks given to the
1624 constructor."""
1625 data = []
1626 self._addFuelData(data)
1627 data.append( ("sim/aircraft/weight/acf_m_fuel_tot", TYPE_FLOAT) )
1628 data.append( ("sim/aircraft/overflow/acf_tank_rat",
1629 (TYPE_FLOAT_ARRAY, len(self._fuelTanks)) ) )
1630
1631 handler.requestRead(data, self._handleFuelRetrieved,
1632 extra = callback)
1633
1634 def setFuelLevel(self, handler, levels):
1635 """Set the fuel level.
1636
1637 See the description of Simulator.setFuelLevel. This
1638 implementation simply sets the fuel tanks as given."""
1639 data = []
1640 for (tank, level) in levels:
1641 try:
1642 index = self._fuelTanks.index(tank)
1643 data.append( ("sim/flightmodel/weight/m_fuel",
1644 (TYPE_FLOAT_ARRAY, 1, index),
1645 [level * self._fuelTankCapacities[index]]) )
1646 except:
1647 print("xplane.Simulator.setFuelLevel: invalid tank constant: %d" % \
1648 (tank,))
1649
1650 handler.requestWrite(data, self._handleFuelWritten)
1651
1652 def _addFuelData(self, data):
1653 """Add the fuel offsets to the given data array.
1654
1655 Returns the index of the first fuel tank's data."""
1656 fuelStartIndex = len(data)
1657 data.append( ("sim/flightmodel/weight/m_fuel",
1658 (TYPE_FLOAT_ARRAY, len(self._fuelTanks) ) ) )
1659
1660 return fuelStartIndex
1661
1662 def _convertFuelData(self, data, index = 0, addCapacities = False):
1663 """Convert the given data into a fuel info list.
1664
1665 The list consists of two or three-tuples of the following
1666 items:
1667 - the fuel tank ID,
1668 - the amount of the fuel in kg,
1669 - if addCapacities is True, the total capacity of the tank."""
1670 fuelWeight = data[index] / 256.0
1671 index += 1
1672
1673 result = []
1674 totalFuel = 0
1675 for fuelTank in self._fuelTanks:
1676 capacity = data[index+1] * fuelWeight * const.LBSTOKG
1677 if capacity>=1.0:
1678 amount = data[index] * capacity / 128.0 / 65536.0
1679
1680 result.append( (fuelTank, amount, capacity) if addCapacities
1681 else (fuelTank, amount))
1682 totalFuel += amount
1683 index += 2
1684
1685 return (result, totalFuel)
1686
1687 def _handleFuelRetrieved(self, data, callback):
1688 """Callback for a fuel retrieval request."""
1689 result = []
1690 totalCapacity = data[1]
1691 for index in range(0, len(self._fuelTanks)):
1692 amount = data[0][index]
1693 capacity = data[2][index] * totalCapacity
1694 self._fuelTankCapacities[index] = capacity
1695 result.append( (self._fuelTanks[index], amount, capacity) )
1696
1697 callback(result)
1698
1699 def _handleFuelWritten(self, success, extra):
1700 """Callback for a fuel setting request."""
1701 pass
1702
1703#------------------------------------------------------------------------------
1704
1705class GenericModel(GenericAircraftModel):
1706 """Generic aircraft model for an unknown type."""
1707 def __init__(self):
1708 """Construct the model."""
1709 super(GenericModel, self). \
1710 __init__(flapsNotches = [0, 10, 20, 30],
1711 fuelTanks = [const.FUELTANK_LEFT, const.FUELTANK_RIGHT],
1712 numEngines = 2)
1713
1714 @property
1715 def name(self):
1716 """Get the name for this aircraft model."""
1717 return "X-Plane/Generic"
1718
1719#------------------------------------------------------------------------------
1720
1721class B737Model(GenericAircraftModel):
1722 """Generic model for the Boeing 737 Classing and NG aircraft."""
1723 fuelTanks = [const.FUELTANK_LEFT, const.FUELTANK_CENTRE, const.FUELTANK_RIGHT]
1724
1725 def __init__(self):
1726 """Construct the model."""
1727 super(B737Model, self). \
1728 __init__(flapsNotches = [0, 1, 2, 5, 10, 15, 25, 30, 40],
1729 fuelTanks = B737Model.fuelTanks,
1730 numEngines = 2)
1731
1732 @property
1733 def name(self):
1734 """Get the name for this aircraft model."""
1735 return "X-Plane/Generic Boeing 737"
1736
1737#------------------------------------------------------------------------------
1738
1739class ZiboB737NGModel(B737Model):
1740 """Base model for the Zibo and LevelUp Boeing 737 models."""
1741 def __init__(self, flapsRatios = [0.0, 0.081633, 0.142857, 0.224490,
1742 0.285714, 0.367347, 0.551020, 0.714286,
1743 1.0]):
1744 super(ZiboB737NGModel, self).__init__()
1745 self._flapsRatios = flapsRatios
1746
1747 def addMonitoringData(self, data, fsType):
1748 """Add the model-specific monitoring data to the given array."""
1749 super(ZiboB737NGModel, self).addMonitoringData(data, fsType)
1750
1751 self._speedBrakeIndex = len(data)
1752 self._addDatarefWithIndexMember(data,
1753 "sim/flightmodel2/wing/speedbrake1_deg",
1754 (TYPE_FLOAT_ARRAY, 2))
1755 self._addDatarefWithIndexMember(data,
1756 "sim/flightmodel2/wing/speedbrake2_deg",
1757 (TYPE_FLOAT_ARRAY, 2))
1758 self._cgIndex = len(data)
1759 self._addDatarefWithIndexMember(data,
1760 "laminar/B738/tab/cg_pos",
1761 TYPE_FLOAT)
1762 self._wingHeatIndex = len(data)
1763 self._addDatarefWithIndexMember(data,
1764 "laminar/B738/ice/wing_heat_pos",
1765 TYPE_FLOAT)
1766 self._eng1HeatIndex = len(data)
1767 self._addDatarefWithIndexMember(data,
1768 "laminar/B738/ice/eng1_heat_pos",
1769 TYPE_FLOAT)
1770 self._eng2HeatIndex = len(data)
1771 self._addDatarefWithIndexMember(data,
1772 "laminar/B738/ice/eng2_heat_pos",
1773 TYPE_FLOAT)
1774 self._spoilersArmedIndex = len(data)
1775 self._addDatarefWithIndexMember(data,
1776 "laminar/B738/annunciator/speedbrake_armed",
1777 TYPE_FLOAT)
1778
1779 self._apCMDStatusIndex = len(data)
1780 self._addDatarefWithIndexMember(data,
1781 "laminar/B738/autopilot/cmd_a_status",
1782 TYPE_FLOAT)
1783 self._addDatarefWithIndexMember(data,
1784 "laminar/B738/autopilot/cmd_b_status",
1785 TYPE_FLOAT)
1786
1787 self._apHeadingIndex = len(data)
1788 self._addDatarefWithIndexMember(data,
1789 "laminar/B738/autopilot/hdg_sel_status",
1790 TYPE_FLOAT)
1791 self._addDatarefWithIndexMember(data,
1792 "laminar/B738/hud/hdg_bug_tape",
1793 TYPE_FLOAT)
1794
1795 self._apAltitudeIndex = len(data)
1796 self._addDatarefWithIndexMember(data,
1797 "laminar/B738/autopilot/alt_hld_status",
1798 TYPE_FLOAT)
1799
1800
1801 def getAircraftState(self, aircraft, timestamp, data):
1802 """Get the aircraft state."""
1803 state = super(ZiboB737NGModel, self).getAircraftState(aircraft,
1804 timestamp,
1805 data)
1806 state.cog = data[self._cgIndex]/100.0
1807
1808 flapsRatios = self._flapsRatios
1809 flapsRatio = data[self._monidx_flapsLeft]
1810 index = len(flapsRatios)
1811 for i in range(1, len(flapsRatios)):
1812 if flapsRatio<flapsRatios[i]:
1813 index = i-1
1814 break
1815 if index<len(flapsRatios):
1816 flapsRatio0 = flapsRatios[index]
1817 flapsNotch0 = self._flapsNotches[index]
1818 state.flaps = flapsNotch0 + \
1819 (self._flapsNotches[index+1] - flapsNotch0) * \
1820 (flapsRatio - flapsRatio0) / \
1821 (flapsRatios[index+1] - flapsRatio0)
1822 else:
1823 state.flaps = self._flapsNotches[-1]
1824
1825 # 0 -> -1
1826 # 15 -> 0.790881
1827 state.elevatorTrim = \
1828 15.0 * (data[self._monidx_elevatorTrim] + 1) / 1.790881
1829
1830 state.spoilersExtension = \
1831 sum(data[self._speedBrakeIndex] + data[self._speedBrakeIndex+1])/4
1832
1833 state.antiIceOn = data[self._wingHeatIndex]!=0 or \
1834 data[self._eng1HeatIndex]!=0 or \
1835 data[self._eng2HeatIndex]!=0
1836
1837 state.spoilersArmed = data[self._spoilersArmedIndex]!=0
1838
1839 state.apMaster = \
1840 data[self._apCMDStatusIndex]==1 or \
1841 data[self._apCMDStatusIndex+1]==1
1842
1843 mcpHeadingHoldStatus = data[self._apHeadingIndex]
1844 mcpHeadingBugStatus = data[self._apHeadingIndex+1]
1845 state.apHeadingHold = mcpHeadingHoldStatus!=0 and \
1846 (mcpHeadingBugStatus<=0.5 and mcpHeadingBugStatus>=-0.5)
1847
1848 state.apAltitudeHold = data[self._apAltitudeIndex]==1
1849
1850 return state
1851
1852#------------------------------------------------------------------------------
1853
1854class ZiboB738Model(ZiboB737NGModel):
1855 """Model for the Zibo Boeing 737-800 model."""
1856 @staticmethod
1857 def doesHandle(aircraft, data):
1858 """Determine if this model handler handles the aircraft with the given
1859 name."""
1860 (tailnum, author, description, notes, icao, liveryPath) = data
1861 return author=="Alex Unruh" and \
1862 description=="Boeing 737-800X" and \
1863 notes.startswith("ZIBOmod") and \
1864 icao=="B738"
1865
1866 def __init__(self):
1867 """Construct the model."""
1868 super(ZiboB738Model, self).__init__(
1869 flapsRatios = [0.0, 0.081633, 0.142857, 0.224490, 0.285714, 0.346939,
1870 0.551020, 0.673469, 1.0])
1871
1872 @property
1873 def name(self):
1874 """Get the name for this aircraft model."""
1875 return "Zibo Boeing 737-800"
1876
1877#------------------------------------------------------------------------------
1878
1879class LevelUpB736Model(ZiboB737NGModel):
1880 """Model for the LevelUp Boeing 737-600 model."""
1881
1882 @staticmethod
1883 def doesHandle(aircraft, data):
1884 """Determine if this model handler handles the aircraft with the given
1885 name."""
1886 (tailnum, author, description, notes, icao, liveryPath) = data
1887 return author=="Alex Unruh" and \
1888 description=="Boeing 737-600NG" and \
1889 icao=="B736"
1890
1891 @property
1892 def name(self):
1893 """Get the name for this aircraft model."""
1894 return "LevelUp Boeing 737-600"
1895
1896#------------------------------------------------------------------------------
1897
1898class LevelUpB737Model(ZiboB737NGModel):
1899 """Model for the LevelUp Boeing 737-700 model."""
1900
1901 @staticmethod
1902 def doesHandle(aircraft, data):
1903 """Determine if this model handler handles the aircraft with the given
1904 name."""
1905 (tailnum, author, description, notes, icao, liveryPath) = data
1906 return author=="Alex Unruh" and \
1907 description=="Boeing 737-700NG" and \
1908 icao=="B737"
1909
1910 @property
1911 def name(self):
1912 """Get the name for this aircraft model."""
1913 return "LevelUp Boeing 737-700"
1914
1915#------------------------------------------------------------------------------
1916
1917class LevelUpB738Model(ZiboB737NGModel):
1918 """Model for the LevelUp Boeing 737-800 model."""
1919
1920 @staticmethod
1921 def doesHandle(aircraft, data):
1922 """Determine if this model handler handles the aircraft with the given
1923 name."""
1924 (tailnum, author, description, notes, icao, liveryPath) = data
1925 return author=="Alex Unruh" and \
1926 description=="Boeing 737-800NG" and \
1927 icao=="B738"
1928
1929 @property
1930 def name(self):
1931 """Get the name for this aircraft model."""
1932 return "LevelUp Boeing 737-800"
1933
1934#------------------------------------------------------------------------------
1935
1936class B767Model(GenericAircraftModel):
1937 """Generic model for the Boeing 767 aircraft."""
1938 fuelTanks = [const.FUELTANK_LEFT, const.FUELTANK_CENTRE, const.FUELTANK_RIGHT]
1939
1940 def __init__(self):
1941 """Construct the model."""
1942 super(B767Model, self). \
1943 __init__(flapsNotches = [0, 1, 5, 15, 20, 25, 30],
1944 fuelTanks = B767Model.fuelTanks,
1945 numEngines = 2)
1946
1947 @property
1948 def name(self):
1949 """Get the name for this aircraft model."""
1950 return "X-Plane/Generic Boeing 767"
1951
1952#------------------------------------------------------------------------------
1953
1954class DH8DModel(GenericAircraftModel):
1955 """Generic model for the Bombardier Dash 8-Q400 aircraft."""
1956 fuelTanks = [const.FUELTANK_LEFT, const.FUELTANK_RIGHT]
1957
1958 def __init__(self):
1959 """Construct the model."""
1960 super(DH8DModel, self). \
1961 __init__(flapsNotches = [0, 5, 10, 15, 35],
1962 fuelTanks = DH8DModel.fuelTanks,
1963 numEngines = 2)
1964
1965 @property
1966 def name(self):
1967 """Get the name for this aircraft model."""
1968 return "X-Plane/Generic Bombardier Dash 8-Q400"
1969
1970#------------------------------------------------------------------------------
1971
1972class FJSDH8DModel(DH8DModel):
1973 """Model handler for the FlyJSim Dash 8-Q400."""
1974 @staticmethod
1975 def doesHandle(aircraft, data):
1976 """Determine if this model handler handles the aircraft with the given
1977 name."""
1978 (tailnum, author, description, notes, icao, liveryPath) = data
1979 return aircraft.type==const.AIRCRAFT_DH8D and \
1980 description.find("Dash 8 Q400")!=-1 and \
1981 ((author in ["2012", "2013"] and tailnum=="N62890") or \
1982 author.find("Jack Skieczius")!=-1)
1983
1984 @property
1985 def name(self):
1986 """Get the name for this aircraft model."""
1987 return "X-Plane/FlyJSim Bombardier Dash 8-Q400"
1988
1989 def addMonitoringData(self, data, fsType):
1990 """Add the model-specific monitoring data to the given array."""
1991 super(FJSDH8DModel, self).addMonitoringData(data, fsType)
1992
1993 self._speedBrakeIndex = len(data)
1994 self._addDatarefWithIndexMember(data,
1995 "sim/flightmodel2/wing/speedbrake1_deg",
1996 (TYPE_FLOAT_ARRAY, 2))
1997 self._addDatarefWithIndexMember(data,
1998 "sim/flightmodel2/wing/speedbrake2_deg",
1999 (TYPE_FLOAT_ARRAY, 2))
2000
2001
2002 def getAircraftState(self, aircraft, timestamp, data):
2003 """Get the aircraft state.
2004
2005 Get it from the parent, and then invert the pitot heat state."""
2006 state = super(FJSDH8DModel, self).getAircraftState(aircraft,
2007 timestamp,
2008 data)
2009 state.antiCollisionLightsOn = \
2010 state.antiCollisionLightsOn or state.strobeLightsOn
2011 state.cog = (state.cog / 0.0254 + 21.504) / 94.512
2012
2013 # It seems that N1 does not always go down to 0 properly
2014 # (maybe due to winds?)
2015 state.n1 = [0 if n1<2.0 else n1 for n1 in state.n1]
2016
2017 state.spoilersExtension = \
2018 sum(data[self._speedBrakeIndex] + data[self._speedBrakeIndex+1])/4
2019
2020 return state
2021
2022#------------------------------------------------------------------------------
2023
2024class FJSDH8DXPModel(DH8DModel):
2025 """Model handler for the FlyJSim Q4XP."""
2026 @staticmethod
2027 def doesHandle(aircraft, data):
2028 """Determine if this model handler handles the aircraft with the given
2029 name."""
2030 (tailnum, author, description, notes, icao, liveryPath) = data
2031 return aircraft.type==const.AIRCRAFT_DH8D and \
2032 description.find("Dash 8 Q400")!=-1 and \
2033 author=="FlyJSim" and tailnum=="N62890"
2034
2035 @property
2036 def name(self):
2037 """Get the name for this aircraft model."""
2038 return "X-Plane/FlyJSim Q4XP"
2039
2040 def addMonitoringData(self, data, fsType):
2041 """Add the model-specific monitoring data to the given array."""
2042 super(FJSDH8DXPModel, self).addMonitoringData(data, fsType)
2043
2044 self._speedBrakeIndex = len(data)
2045 self._addDatarefWithIndexMember(data,
2046 "sim/flightmodel2/wing/spoiler1_deg",
2047 (TYPE_FLOAT_ARRAY, 32))
2048 self._addDatarefWithIndexMember(data,
2049 "sim/flightmodel2/wing/spoiler2_deg",
2050 (TYPE_FLOAT_ARRAY, 32))
2051
2052 self._gearIndex = len(data)
2053 self._addDatarefWithIndexMember(data,
2054 "FJS/Q4XP/Manips/GearDeployHandle_Ctl",
2055 (TYPE_FLOAT_ARRAY, 1))
2056
2057 self._apIndex = len(data)
2058 self._addDatarefWithIndexMember(data,
2059 "FJS/Q4XP/FMA/roll_act",
2060 TYPE_INT)
2061 self._addDatarefWithIndexMember(data,
2062 "FJS/Q4XP/FMA/pitch_act",
2063 TYPE_INT)
2064
2065 self._propPitchIndex = len(data)
2066 self._addDatarefWithIndexMember(data,
2067 "sim/flightmodel2/engines/prop_pitch_deg",
2068 (TYPE_FLOAT_ARRAY, 2))
2069
2070
2071 def getAircraftState(self, aircraft, timestamp, data):
2072 """Get the aircraft state.
2073
2074 Get it from the parent, and then invert the pitot heat state."""
2075 state = super(FJSDH8DXPModel, self).getAircraftState(aircraft,
2076 timestamp,
2077 data)
2078 state.antiCollisionLightsOn = \
2079 state.antiCollisionLightsOn or state.strobeLightsOn
2080 state.cog = (state.cog * 41.656436697 + 27.586779769)/100.0
2081
2082 # It seems that N1 does not always go down to 0 properly
2083 # (maybe due to winds?)
2084 state.n1 = [0 if n1<2.0 else n1 for n1 in state.n1]
2085
2086 state.spoilersExtension = \
2087 sum(data[self._speedBrakeIndex] + data[self._speedBrakeIndex+1])/4
2088 if state.spoilersExtension<40:
2089 state.spoilersExtension = 0.0
2090
2091 state.gearControlDown = data[self._gearIndex][0]>0.5
2092
2093 state.apHeadingHold = data[self._apIndex]==4
2094 state.apAltitudeHold = data[self._apIndex+1] in [4, 5]
2095
2096 state.reverser = [p<=-12.0 for p in data[self._propPitchIndex]]
2097
2098 return state
2099
2100#------------------------------------------------------------------------------
2101
2102class CRJ2Model(GenericAircraftModel):
2103 """Generic model for the Bombardier CRJ-200 aircraft."""
2104 fuelTanks = [const.FUELTANK_LEFT, const.FUELTANK_CENTRE, const.FUELTANK_RIGHT]
2105
2106 def __init__(self):
2107 """Construct the model."""
2108 super(CRJ2Model, self). \
2109 __init__(flapsNotches = [0, 8, 20, 30, 45],
2110 fuelTanks = CRJ2Model.fuelTanks,
2111 numEngines = 2)
2112
2113 @property
2114 def name(self):
2115 """Get the name for this aircraft model."""
2116 return "X-Plane/Generic Bombardier CRJ-200"
2117
2118#------------------------------------------------------------------------------
2119
2120class F70Model(GenericAircraftModel):
2121 """Generic model for the Fokker F70 aircraft."""
2122 fuelTanks = [const.FUELTANK_LEFT, const.FUELTANK_CENTRE, const.FUELTANK_RIGHT]
2123
2124 def __init__(self):
2125 """Construct the model."""
2126 super(F70Model, self). \
2127 __init__(flapsNotches = [0, 8, 15, 25, 42],
2128 fuelTanks = F70Model.fuelTanks,
2129 numEngines = 2)
2130
2131 @property
2132 def name(self):
2133 """Get the name for this aircraft model."""
2134 return "X-Plane/Generic Fokker 70"
2135
2136#------------------------------------------------------------------------------
2137
2138class DC3Model(GenericAircraftModel):
2139 """Generic model for the Lisunov Li-2 (DC-3) aircraft."""
2140 fuelTanks = [const.FUELTANK_LEFT, const.FUELTANK_CENTRE,
2141 const.FUELTANK_RIGHT]
2142 # fuelTanks = [const.FUELTANK_LEFT_AUX, const.FUELTANK_LEFT,
2143 # const.FUELTANK_RIGHT, const.FUELTANK_RIGHT_AUX]
2144
2145 def __init__(self):
2146 """Construct the model."""
2147 super(DC3Model, self). \
2148 __init__(flapsNotches = [0, 15, 30, 45],
2149 fuelTanks = DC3Model.fuelTanks,
2150 numEngines = 2, isN1 = False)
2151 self._leftLevel = 0.0
2152 self._rightLevel = 0.0
2153
2154 @property
2155 def name(self):
2156 """Get the name for this aircraft model."""
2157 return "X-Plane/Generic Lisunov Li-2 (DC-3)"
2158
2159#------------------------------------------------------------------------------
2160
2161class T134Model(GenericAircraftModel):
2162 """Generic model for the Tupolev Tu-134 aircraft."""
2163 fuelTanks = [const.FUELTANK_LEFT_TIP, const.FUELTANK_EXTERNAL1,
2164 const.FUELTANK_LEFT_AUX,
2165 const.FUELTANK_CENTRE,
2166 const.FUELTANK_RIGHT_AUX,
2167 const.FUELTANK_EXTERNAL2, const.FUELTANK_RIGHT_TIP]
2168
2169 def __init__(self):
2170 """Construct the model."""
2171 super(T134Model, self). \
2172 __init__(flapsNotches = [0, 10, 20, 30],
2173 fuelTanks = T134Model.fuelTanks,
2174 numEngines = 2)
2175
2176 @property
2177 def name(self):
2178 """Get the name for this aircraft model."""
2179 return "X-Plane/Generic Tupolev Tu-134"
2180
2181#------------------------------------------------------------------------------
2182
2183class T154Model(GenericAircraftModel):
2184 """Generic model for the Tupolev Tu-154 aircraft."""
2185 fuelTanks = [const.FUELTANK_CENTRE, const.FUELTANK_CENTRE2,
2186 const.FUELTANK_RIGHT, const.FUELTANK_LEFT,
2187 const.FUELTANK_RIGHT_AUX, const.FUELTANK_LEFT_AUX]
2188
2189 def __init__(self):
2190 """Construct the model."""
2191 super(T154Model, self). \
2192 __init__(flapsNotches = [0, 15, 28, 45],
2193 fuelTanks = T154Model.fuelTanks,
2194 numEngines = 3)
2195
2196 @property
2197 def name(self):
2198 """Get the name for this aircraft model."""
2199 return "X-Plane/Generic Tupolev Tu-154"
2200
2201 def getAircraftState(self, aircraft, timestamp, data):
2202 """Get an aircraft state object for the given monitoring data.
2203
2204 This removes the reverser value for the middle engine."""
2205 state = super(T154Model, self).getAircraftState(aircraft, timestamp, data)
2206 del state.reverser[1]
2207 return state
2208
2209#------------------------------------------------------------------------------
2210
2211class FelisT154Model(T154Model):
2212 """Model for Felis' Tupolev Tu-154-M aircraft."""
2213 @staticmethod
2214 def doesHandle(aircraft, data):
2215 """Determine if this model handler handles the aircraft with the given
2216 name."""
2217 (tailnum, author, description, notes, icao, liveryPath) = data
2218 return aircraft.type==const.AIRCRAFT_T154 and \
2219 author.find("Felis")!=-1 and \
2220 description.find("Tu154M")!=-1
2221
2222 def __init__(self):
2223 """Construct the model."""
2224 super(T154Model, self). \
2225 __init__(flapsNotches = [0, 15, 28, 36, 45],
2226 fuelTanks = T154Model.fuelTanks,
2227 numEngines = 3)
2228
2229 @property
2230 def name(self):
2231 """Get the name for this aircraft model."""
2232 return "X-Plane/Felis Tupolev Tu-154-M"
2233
2234#------------------------------------------------------------------------------
2235
2236class YK40Model(GenericAircraftModel):
2237 """Generic model for the Yakovlev Yak-40 aircraft."""
2238 fuelTanks = [const.FUELTANK_LEFT, const.FUELTANK_RIGHT]
2239
2240 def __init__(self):
2241 """Construct the model."""
2242 super(YK40Model, self). \
2243 __init__(flapsNotches = [0, 20, 35],
2244 fuelTanks = YK40Model.fuelTanks,
2245 numEngines = 2)
2246
2247 @property
2248 def name(self):
2249 """Get the name for this aircraft model."""
2250 return "X-Plane/Generic Yakovlev Yak-40"
2251
2252#------------------------------------------------------------------------------
2253
2254_genericModels = { const.AIRCRAFT_B736 : B737Model,
2255 const.AIRCRAFT_B737 : B737Model,
2256 const.AIRCRAFT_B738 : B737Model,
2257 const.AIRCRAFT_B738C : B737Model,
2258 const.AIRCRAFT_B732 : B737Model,
2259 const.AIRCRAFT_B733 : B737Model,
2260 const.AIRCRAFT_B734 : B737Model,
2261 const.AIRCRAFT_B735 : B737Model,
2262 const.AIRCRAFT_DH8D : DH8DModel,
2263 const.AIRCRAFT_B762 : B767Model,
2264 const.AIRCRAFT_B763 : B767Model,
2265 const.AIRCRAFT_CRJ2 : CRJ2Model,
2266 const.AIRCRAFT_F70 : F70Model,
2267 const.AIRCRAFT_DC3 : DC3Model,
2268 const.AIRCRAFT_T134 : T134Model,
2269 const.AIRCRAFT_T154 : T154Model,
2270 const.AIRCRAFT_YK40 : YK40Model }
2271
2272#------------------------------------------------------------------------------
2273
2274AircraftModel.registerSpecial(ZiboB738Model)
2275AircraftModel.registerSpecial(LevelUpB736Model)
2276AircraftModel.registerSpecial(LevelUpB737Model)
2277AircraftModel.registerSpecial(LevelUpB738Model)
2278AircraftModel.registerSpecial(FJSDH8DModel)
2279AircraftModel.registerSpecial(FJSDH8DXPModel)
2280AircraftModel.registerSpecial(FelisT154Model)
2281
2282#------------------------------------------------------------------------------
2283
2284# if __name__ == "__main__":
2285# class ConnectionListener:
2286# def connected(self, fsType, descriptor):
2287# """Called when a connection has been established to the flight
2288# simulator of the given type."""
2289# print "fs.ConnectionListener.connected, fsType:", fsType, ", descriptor:", descriptor
2290
2291# def connectionFailed(self):
2292# """Called when the connection could not be established."""
2293# print "fs.ConnectionListener.connectionFailed"
2294
2295# def disconnected(self):
2296# """Called when a connection to the flight simulator has been broken."""
2297# print "fs.ConnectionListener.disconnected"
2298
2299# class Config:
2300# def __init__(self):
2301# self.onlineACARS = False
2302# self.realIASSmoothingLength = 2
2303# self.realVSSmoothingLength = 2
2304# self.enableSounds = False
2305# self.usingFS2Crew = False
2306
2307# def isMessageTypeFS(self, type):
2308# return True
2309
2310
2311
2312# class GUI:
2313# def __init__(self):
2314# self.config = Config()
2315# self.entranceExam = False
2316# self.zfw = 30000.0
2317
2318# def resetFlightStatus(self):
2319# pass
2320
2321# def setRating(self, value):
2322# pass
2323
2324# def insertFlightLogLine(self, index, ts, text, isFault):
2325# pass
2326
2327# def setStage(self, stage):
2328# pass
2329
2330
2331# from i18n import setLanguage
2332
2333# setLanguage("/home/vi/munka/repules/mlx", "en")
2334
2335# from logger import Logger
2336# from flight import Flight
2337# from acft import DH8D
2338
2339# gui = GUI()
2340
2341# logger = Logger(gui)
2342
2343# flight = Flight(logger, gui)
2344# acft = DH8D(flight)
2345
2346# Watchdog()
2347
2348# connectionListener = ConnectionListener()
2349# simulator = Simulator(connectionListener, connectAttempts = 3)
2350
2351# simulator.connect(acft)
2352
2353# time.sleep(2)
2354
2355# simulator.startMonitoring()
2356
2357# simulator.sendMessage("[MLX] Flight stage: Taxi", duration = 3)
2358
2359# time.sleep(4)
2360
2361# 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)
2362# #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)
2363# #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)
2364
2365# time.sleep(30)
2366
2367# simulator.sendMessage("[MLX] Hello", duration = 3)
2368
2369# time.sleep(10)
Note: See TracBrowser for help on using the repository browser.