source: src/mlx/xplane.py@ 1136:fdb94a9baec1

python3
Last change on this file since 1136:fdb94a9baec1 was 1136:fdb94a9baec1, checked in by István Váradi <ivaradi@…>, 9 months ago

A better dataref is used to get the centre of gravity in case of Zibo/LevelUp models

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