source: src/mlx/xplane.py@ 537:ba6d27d49639

Last change on this file since 537:ba6d27d49639 was 535:f42fe0ead68f, checked in by István Váradi <ivaradi@…>, 11 years ago

Implemented watchdog monitoring the FSUIPC and X-Plane connection handlers

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