source: src/mlx/xplane.py@ 1064:d70786515883

python3 version_0.51
Last change on this file since 1064:d70786515883 was 1064:d70786515883, checked in by István Váradi <ivaradi@…>, 19 months ago

Support for the FlyJSim Q4XP model

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