source: src/mlx/xplane.py@ 1067:9ce88d0b881d

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

Support for connecting to X-Plane remotely

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