source: src/mlx/xplane.py@ 1071:1a5ab6d7fe1b

python3
Last change on this file since 1071:1a5ab6d7fe1b was 1070:6df48aba546b, checked in by István Váradi <ivaradi@…>, 2 years ago

Only non-periodic requests are put back into the queue on connection failure

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