source: src/mlx/xplane.py@ 1143:dbe6c974f227

python3
Last change on this file since 1143:dbe6c974f227 was 1137:87a29e5e4b2d, checked in by István Váradi <ivaradi@…>, 8 months ago

The G-load is measured during flare and the value at touchdown is logged (re #385)

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