source: src/mlx/xplane.py@ 854:3e7dca86c1ed

Last change on this file since 854:3e7dca86c1ed was 790:e69a08004f40, checked in by István Váradi <ivaradi@…>, 8 years ago

Added support for the Boeing 737-200 aircraft type (weight data is still missing, re #302)

File size: 76.2 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 def connect(self, aircraft):
736 """Initiate a connection to the simulator."""
737 self._aircraft = aircraft
738 self._aircraftInfo = None
739 self._aircraftModel = None
740 self._handler.connect()
741 if self._normalRequestID is None:
742 self._nextSyncTime = -1
743 self._startDefaultNormal()
744
745 def reconnect(self):
746 """Initiate a reconnection to the simulator.
747
748 It does not reset already set up data, just calls connect() on the
749 handler."""
750 self._handler.connect()
751
752 def requestZFW(self, callback):
753 """Send a request for the ZFW."""
754 data = [ ("sim/aircraft/weight/acf_m_empty", TYPE_FLOAT),
755 ("sim/flightmodel/weight/m_fixed", TYPE_FLOAT) ]
756 self._handler.requestRead(data, self._handleZFW, extra = callback)
757
758 def requestWeights(self, callback):
759 """Request the following weights: DOW, ZFW, payload.
760
761 These values will be passed to the callback function in this order, as
762 separate arguments."""
763 data = [ ("sim/aircraft/weight/acf_m_empty", TYPE_FLOAT),
764 ("sim/flightmodel/weight/m_fixed", TYPE_FLOAT),
765 ("sim/flightmodel/weight/m_total", TYPE_FLOAT) ]
766 self._handler.requestRead(data, self._handleWeights,
767 extra = callback)
768
769 def requestTime(self, callback):
770 """Request the time from the simulator."""
771 self._handler.requestRead(Simulator.timeData, self._handleTime,
772 extra = callback)
773
774 def startMonitoring(self):
775 """Start the periodic monitoring of the aircraft and pass the resulting
776 state to the aircraft object periodically."""
777 assert not self._monitoringRequested
778 self._monitoringRequested = True
779
780 def stopMonitoring(self):
781 """Stop the periodic monitoring of the aircraft."""
782 assert self._monitoringRequested
783 self._monitoringRequested = False
784
785 def startFlare(self):
786 """Start monitoring the flare time.
787
788 At present it is assumed to be called from the handler thread, hence no
789 protection."""
790 #self._aircraft.logger.debug("startFlare")
791 if self._flareRequestID is None:
792 self._flareRates = []
793 self._flareRequestID = \
794 self._handler.requestPeriodicRead(0.1,
795 Simulator.flareData1,
796 self._handleFlare1)
797
798 def cancelFlare(self):
799 """Cancel monitoring the flare time.
800
801 At present it is assumed to be called from the handler thread, hence no
802 protection."""
803 if self._flareRequestID is not None:
804 self._handler.clearPeriodic(self._flareRequestID)
805 self._flareRequestID = None
806
807 def sendMessage(self, message, duration = 3,
808 _disconnect = False):
809 """Send a message to the pilot via the simulator.
810
811 duration is the number of seconds to keep the message displayed."""
812 print "xplra.Simulator.sendMessage:", message
813 self._handler.requestShowMessage(message, duration,
814 self._handleMessageSent,
815 extra = _disconnect)
816
817 def getFuel(self, callback):
818 """Get the fuel information for the current model.
819
820 The callback will be called with a list of triplets with the following
821 items:
822 - the fuel tank identifier
823 - the current weight of the fuel in the tank (in kgs)
824 - the current total capacity of the tank (in kgs)."""
825 if self._aircraftModel is None:
826 self._fuelCallback = callback
827 else:
828 self._aircraftModel.getFuel(self._handler, callback)
829
830 def setFuelLevel(self, levels):
831 """Set the fuel level to the given ones.
832
833 levels is an array of two-tuples, where each tuple consists of the
834 following:
835 - the const.FUELTANK_XXX constant denoting the tank that must be set,
836 - the requested level of the fuel as a floating-point value between 0.0
837 and 1.0."""
838 if self._aircraftModel is not None:
839 self._aircraftModel.setFuelLevel(self._handler, levels)
840
841 def enableTimeSync(self):
842 """Enable the time synchronization."""
843 self._nextSyncTime = -1
844 self._syncTime = True
845
846 def disableTimeSync(self):
847 """Enable the time synchronization."""
848 self._syncTime = False
849 self._nextSyncTime = -1
850
851 def listenHotkeys(self, hotkeys, callback):
852 """Start listening to the given hotkeys.
853
854 callback is function expecting two arguments:
855 - the ID of the hotkey set as returned by this function,
856 - the list of the indexes of the hotkeys that were pressed."""
857 with self._hotkeyLock:
858 assert self._hotkeyCodes is None
859
860 self._hotkeyCodes = \
861 [self._getHotkeyCode(hotkey) for hotkey in hotkeys]
862 self._hotkeySetID += 1
863 self._hotkeySetGeneration = 0
864 self._hotkeyCallback = callback
865
866 self._handler.registerHotkeys(self._hotkeyCodes,
867 self._handleHotkeysRegistered,
868 (self._hotkeySetID,
869 self._hotkeySetGeneration))
870
871 return self._hotkeySetID
872
873 def clearHotkeys(self):
874 """Clear the current hotkey set.
875
876 Note that it is possible, that the callback function set either
877 previously or after calling this function by listenHotkeys() will be
878 called with data from the previous hotkey set.
879
880 Therefore it is recommended to store the hotkey set ID somewhere and
881 check that in the callback function. Right before calling
882 clearHotkeys(), this stored ID should be cleared so that the check
883 fails for sure."""
884 with self._hotkeyLock:
885 if self._hotkeyCodes is not None:
886 self._hotkeyCodes = None
887 self._hotkeySetID += 1
888 self._hotkeyCallback = None
889 self._clearHotkeyRequest()
890
891 def disconnect(self, closingMessage = None, duration = 3):
892 """Disconnect from the simulator."""
893 assert not self._monitoringRequested
894
895 print "xplra.Simulator.disconnect", closingMessage, duration
896
897 self._stopNormal()
898 self.clearHotkeys()
899 if closingMessage is None:
900 self._handler.disconnect()
901 else:
902 self.sendMessage(closingMessage, duration = duration,
903 _disconnect = True)
904
905 def connected(self, fsType, descriptor):
906 """Called when a connection has been established to the flight
907 simulator of the given type."""
908 self._fsType = fsType
909
910 with self._hotkeyLock:
911 if self._hotkeyCodes is not None:
912 self._hotkeySetGeneration += 1
913
914 self._handler.registerHotkeys(self._hotkeyCodes,
915 self._handleHotkeysRegistered,
916 (self._hotkeySetID,
917 self._hotkeySetGeneration))
918
919 self._connectionListener.connected(fsType, descriptor)
920
921 def connectionFailed(self):
922 """Called when the connection could not be established."""
923 with self._hotkeyLock:
924 self._clearHotkeyRequest()
925 self._connectionListener.connectionFailed()
926
927 def disconnected(self):
928 """Called when a connection to the flight simulator has been broken."""
929 with self._hotkeyLock:
930 self._clearHotkeyRequest()
931 self._connectionListener.disconnected()
932
933 def _startDefaultNormal(self):
934 """Start the default normal periodic request."""
935 assert self._normalRequestID is None
936 self._normalRequestID = \
937 self._handler.requestPeriodicRead(1.0,
938 Simulator.normalData,
939 self._handleNormal)
940
941 def _stopNormal(self):
942 """Stop the normal period request."""
943 assert self._normalRequestID is not None
944 self._handler.clearPeriodic(self._normalRequestID)
945 self._normalRequestID = None
946 self._monitoring = False
947
948 def _handleNormal(self, data, extra):
949 """Handle the reply to the normal request.
950
951 At the beginning the result consists the data for normalData. When
952 monitoring is started, it contains the result also for the
953 aircraft-specific values.
954 """
955 timestamp = Simulator._getTimestamp(data)
956
957 createdNewModel = self._setAircraftName(timestamp,
958 data.getString(2),
959 data.getString(3),
960 data.getString(4),
961 data.getString(5),
962 data.getString(6),
963 data.getString(7))
964 if self._fuelCallback is not None:
965 self._aircraftModel.getFuel(self._handler, self._fuelCallback)
966 self._fuelCallback = None
967
968 if self._monitoringRequested and not self._monitoring:
969 self._stopNormal()
970 self._startMonitoring()
971 elif self._monitoring and not self._monitoringRequested:
972 self._stopNormal()
973 self._startDefaultNormal()
974 elif self._monitoring and self._aircraftModel is not None and \
975 not createdNewModel:
976 aircraftState = self._aircraftModel.getAircraftState(self._aircraft,
977 timestamp, data)
978
979 self._aircraft.handleState(aircraftState)
980
981 def _setAircraftName(self, timestamp, tailnum, author, description,
982 notes, icao, liveryPath):
983 """Set the name of the aicraft and if it is different from the
984 previous, create a new model for it.
985
986 If so, also notifty the aircraft about the change.
987
988 Return if a new model was created."""
989 author = self._latin1decoder(author)[0]
990 description = self._latin1decoder(description)[0]
991 notes = self._latin1decoder(notes)[0]
992 liveryPath = self._latin1decoder(liveryPath)[0]
993
994 aircraftInfo = (tailnum, author, description, notes, icao, liveryPath)
995 if aircraftInfo==self._aircraftInfo:
996 return False
997
998 print "xplane.Simulator: new data: %s, %s, %s, %s, %s, %s" % \
999 (tailnum, author, description, notes, icao, liveryPath)
1000
1001 self._aircraftInfo = aircraftInfo
1002 needNew = self._aircraftModel is None
1003 needNew = needNew or\
1004 not self._aircraftModel.doesHandle(self._aircraft, aircraftInfo)
1005 if not needNew:
1006 specialModel = AircraftModel.findSpecial(self._aircraft,
1007 aircraftInfo)
1008 needNew = specialModel is not None and \
1009 specialModel is not self._aircraftModel.__class__
1010
1011 if needNew:
1012 self._setAircraftModel(AircraftModel.create(self._aircraft,
1013 aircraftInfo))
1014
1015 self._aircraft.modelChanged(timestamp, description,
1016 self._aircraftModel.name)
1017
1018 return needNew
1019
1020 def _setAircraftModel(self, model):
1021 """Set a new aircraft model.
1022
1023 It will be queried for the data to monitor and the monitoring request
1024 will be replaced by a new one."""
1025 self._aircraftModel = model
1026 model.simulator = self
1027
1028 if self._monitoring:
1029 self._stopNormal()
1030 self._startMonitoring()
1031
1032 def _startMonitoring(self):
1033 """Start monitoring with the current aircraft model."""
1034 data = Simulator.normalData[:]
1035 self._aircraftModel.addMonitoringData(data, self._fsType)
1036
1037 self._normalRequestID = \
1038 self._handler.requestPeriodicRead(1.0, data,
1039 self._handleNormal)
1040 self._monitoring = True
1041
1042 def _addFlareRate(self, data):
1043 """Append a flare rate to the list of last rates."""
1044 if len(self._flareRates)>=3:
1045 del self._flareRates[0]
1046 self._flareRates.append(data)
1047
1048 def _handleFlare1(self, data, normal):
1049 """Handle the first stage of flare monitoring."""
1050 #self._aircraft.logger.debug("handleFlare1: " + str(data))
1051 if data[1]<=50.0*0.3048:
1052 self._flareStart = time.time()
1053 self._flareStartFS = data[0]
1054 self._handler.clearPeriodic(self._flareRequestID)
1055 self._handler.requestRead(Simulator.flareStartData,
1056 self._handleFlareStart)
1057
1058 self._addFlareRate(data[2])
1059
1060 def _handleFlareStart(self, data, extra):
1061 """Handle the data need to notify the aircraft about the starting of
1062 the flare."""
1063 #self._aircraft.logger.debug("handleFlareStart: " + str(data))
1064 if data is not None:
1065 windDirection = data[1]
1066 if windDirection<0.0: windDirection += 360.0
1067 self._aircraft.flareStarted(data[0], windDirection, data[2],
1068 self._flareStart, self._flareStartFS)
1069
1070 self._flareRequestID = \
1071 self._handler.requestPeriodicRead(0.1,
1072 Simulator.flareData2,
1073 self._handleFlare2)
1074
1075 def _handleFlare2(self, data, normal):
1076 """Handle the first stage of flare monitoring."""
1077 #self._aircraft.logger.debug("handleFlare2: " + str(data))
1078 if data[1]!=0:
1079 flareEnd = time.time()
1080 self._handler.clearPeriodic(self._flareRequestID)
1081 self._flareRequestID = None
1082
1083 flareEndFS = data[0]
1084 if flareEndFS<self._flareStartFS:
1085 flareEndFS += 86400.0
1086
1087 tdRate = min(self._flareRates)
1088 tdRateCalculatedByFS = False
1089
1090 heading = data[6]
1091 if heading<0.0: heading += 360.0
1092
1093 self._aircraft.flareFinished(flareEnd, flareEndFS,
1094 tdRate, tdRateCalculatedByFS,
1095 data[3], data[4], data[5], heading)
1096 else:
1097 self._addFlareRate(data[2])
1098
1099 def _handleZFW(self, data, callback):
1100 """Callback for a ZFW retrieval request."""
1101 zfw = data[0] + data[1]
1102 callback(zfw)
1103
1104 def _handleTime(self, data, callback):
1105 """Callback for a time retrieval request."""
1106 callback(Simulator._getTimestamp(data))
1107
1108 def _handleWeights(self, data, callback):
1109 """Callback for the weights retrieval request."""
1110 dow = data[0]
1111 payload = data[1]
1112 zfw = dow + payload
1113 grossWeight = data[2]
1114 callback(dow, payload, zfw, grossWeight)
1115
1116 def _handleMessageSent(self, success, disconnect):
1117 """Callback for a message sending request."""
1118 #print "xplra.Simulator._handleMessageSent", disconnect
1119 if disconnect:
1120 self._handler.disconnect()
1121
1122 def _handleHotkeysRegistered(self, success, (id, generation)):
1123 """Handle the result of the hotkeys having been written."""
1124 with self._hotkeyLock:
1125 if success and id==self._hotkeySetID and \
1126 generation==self._hotkeySetGeneration:
1127 self._hotkeyRequestID = \
1128 self._handler.requestHotkeysState(0.5,
1129 self._handleHotkeys,
1130 (id, generation))
1131
1132 def _handleHotkeys(self, data, (id, generation)):
1133 """Handle the hotkeys."""
1134 with self._hotkeyLock:
1135 if id!=self._hotkeySetID or generation!=self._hotkeySetGeneration:
1136 return
1137
1138 callback = self._hotkeyCallback
1139 offsets = self._hotkeyOffets
1140
1141 hotkeysPressed = []
1142 for i in range(0, len(data)):
1143 if data[i]:
1144 hotkeysPressed.append(i)
1145
1146 if hotkeysPressed:
1147 callback(id, hotkeysPressed)
1148
1149 def _clearHotkeyRequest(self):
1150 """Clear the hotkey request in the handler if there is any."""
1151 if self._hotkeyRequestID is not None:
1152 self._handler.unregisterHotkeys(self._hotkeysUnregistered)
1153 self._handler.clearPeriodic(self._hotkeyRequestID)
1154 self._hotkeyRequestID = None
1155
1156 def _hotkeysUnregistered(self, result, extra):
1157 """Called when the hotkeys have been unregistered."""
1158 pass
1159
1160#------------------------------------------------------------------------------
1161
1162class AircraftModel(object):
1163 """Base class for the aircraft models.
1164
1165 Aircraft models handle the data arriving from X-Plane and turn it into an
1166 object describing the aircraft's state."""
1167 monitoringData = [ ("paused",
1168 "sim/time/paused", TYPE_INT),
1169 ("latitude",
1170 "sim/flightmodel/position/latitude", TYPE_DOUBLE),
1171 ("longitude",
1172 "sim/flightmodel/position/longitude", TYPE_DOUBLE),
1173 ("replay",
1174 "sim/operation/prefs/replay_mode", TYPE_INT),
1175 ("overspeed",
1176 "sim/flightmodel/failures/over_vne", TYPE_INT),
1177 ("stalled",
1178 "sim/flightmodel/failures/stallwarning", TYPE_INT),
1179 ("onTheGround",
1180 "sim/flightmodel/failures/onground_any", TYPE_INT),
1181 ("emptyWeight",
1182 "sim/aircraft/weight/acf_m_empty", TYPE_FLOAT),
1183 ("payloadWeight",
1184 "sim/flightmodel/weight/m_fixed", TYPE_FLOAT),
1185 ("grossWeight",
1186 "sim/flightmodel/weight/m_total", TYPE_FLOAT),
1187 ("heading",
1188 "sim/flightmodel/position/psi", TYPE_FLOAT),
1189 ("pitch",
1190 "sim/flightmodel/position/theta", TYPE_FLOAT),
1191 ("bank",
1192 "sim/flightmodel/position/phi", TYPE_FLOAT),
1193 ("ias",
1194 "sim/flightmodel/position/indicated_airspeed2",
1195 TYPE_FLOAT),
1196 ("mach",
1197 "sim/flightmodel/misc/machno", TYPE_FLOAT),
1198 ("groundSpeed",
1199 "sim/flightmodel/position/groundspeed", TYPE_FLOAT),
1200 ("vs",
1201 "sim/flightmodel/position/vh_ind_fpm2", TYPE_FLOAT),
1202 ("radioAltitude",
1203 "sim/flightmodel/position/y_agl", TYPE_FLOAT),
1204 ("altitude",
1205 "sim/flightmodel/position/elevation", TYPE_FLOAT),
1206 ("gLoad",
1207 "sim/flightmodel/forces/g_nrml", TYPE_FLOAT),
1208 ("flapsControl",
1209 "sim/flightmodel/controls/flaprqst", TYPE_FLOAT),
1210 ("flapsLeft",
1211 "sim/flightmodel/controls/flaprat", TYPE_FLOAT),
1212 ("flapsRight",
1213 "sim/flightmodel/controls/flap2rat", TYPE_FLOAT),
1214 ("navLights",
1215 "sim/cockpit/electrical/nav_lights_on", TYPE_INT),
1216 ("beaconLights",
1217 "sim/cockpit/electrical/beacon_lights_on", TYPE_INT),
1218 ("strobeLights",
1219 "sim/cockpit/electrical/strobe_lights_on", TYPE_INT),
1220 ("landingLights",
1221 "sim/cockpit/electrical/landing_lights_on", TYPE_INT),
1222 ("pitot",
1223 "sim/cockpit/switches/pitot_heat_on", TYPE_INT),
1224 ("parking",
1225 "sim/flightmodel/controls/parkbrake", TYPE_FLOAT),
1226 ("gearControl",
1227 "sim/cockpit2/controls/gear_handle_down", TYPE_INT),
1228 ("noseGear",
1229 "sim/flightmodel2/gear/deploy_ratio",
1230 (TYPE_FLOAT_ARRAY, 1)),
1231 ("spoilers",
1232 "sim/flightmodel/controls/lsplrdef", TYPE_FLOAT),
1233 ("altimeter",
1234 "sim/cockpit/misc/barometer_setting", TYPE_FLOAT),
1235 ("qnh",
1236 "sim/physics/earth_pressure_p", TYPE_FLOAT),
1237 ("nav1",
1238 "sim/cockpit/radios/nav1_freq_hz", TYPE_INT),
1239 ("nav1_obs",
1240 "sim/cockpit/radios/nav1_obs_degm", TYPE_FLOAT),
1241 ("nav2",
1242 "sim/cockpit/radios/nav2_freq_hz", TYPE_INT),
1243 ("nav2_obs",
1244 "sim/cockpit/radios/nav2_obs_degm", TYPE_FLOAT),
1245 ("adf1",
1246 "sim/cockpit/radios/adf1_freq_hz", TYPE_INT),
1247 ("adf2",
1248 "sim/cockpit/radios/adf2_freq_hz", TYPE_INT),
1249 ("squawk",
1250 "sim/cockpit/radios/transponder_code", TYPE_INT),
1251 ("windSpeed",
1252 "sim/weather/wind_speed_kt[0]", TYPE_FLOAT),
1253 ("windDirection",
1254 "sim/weather/wind_direction_degt[0]", TYPE_FLOAT),
1255 ("visibility",
1256 "sim/weather/visibility_reported_m", TYPE_FLOAT),
1257 ("cog",
1258 "sim/flightmodel/misc/cgz_ref_to_default", TYPE_FLOAT),
1259 ("xpdrC",
1260 "sim/cockpit/radios/transponder_mode", TYPE_INT),
1261 ("apMaster",
1262 "sim/cockpit/autopilot/autopilot_mode", TYPE_INT),
1263 ("apState",
1264 "sim/cockpit/autopilot/autopilot_state", TYPE_INT),
1265 ("apHeading",
1266 "sim/cockpit/autopilot/heading_mag", TYPE_FLOAT),
1267 ("apAltitude",
1268 "sim/cockpit/autopilot/altitude", TYPE_FLOAT),
1269 ("elevatorTrim",
1270 "sim/flightmodel/controls/elv_trim", TYPE_FLOAT),
1271 ("antiIceOn",
1272 "sim/cockpit/switches/anti_ice_on", TYPE_INT),
1273 ("surfaceHeat",
1274 "sim/cockpit/switches/anti_ice_surf_heat", TYPE_INT),
1275 ("propHeat",
1276 "sim/cockpit/switches/anti_ice_prop_heat", TYPE_INT),
1277 ("autopilotOn",
1278 "sim/cockpit2/autopilot/autopilot_on", TYPE_INT),
1279 ("apHeadingMode",
1280 "sim/cockpit2/autopilot/heading_mode", TYPE_INT)]
1281
1282
1283 specialModels = []
1284
1285 @staticmethod
1286 def registerSpecial(clazz):
1287 """Register the given class as a special model."""
1288 AircraftModel.specialModels.append(clazz)
1289
1290 @staticmethod
1291 def findSpecial(aircraft, aircraftInfo):
1292 for specialModel in AircraftModel.specialModels:
1293 if specialModel.doesHandle(aircraft, aircraftInfo):
1294 return specialModel
1295 return None
1296
1297 @staticmethod
1298 def create(aircraft, aircraftInfo):
1299 """Create the model for the given aircraft name, and notify the
1300 aircraft about it."""
1301 specialModel = AircraftModel.findSpecial(aircraft, aircraftInfo)
1302 if specialModel is not None:
1303 return specialModel()
1304 if aircraft.type in _genericModels:
1305 return _genericModels[aircraft.type]()
1306 else:
1307 return GenericModel()
1308
1309 @staticmethod
1310 def _convertFrequency(value):
1311 """Convert the given frequency value into a string."""
1312 return "%.2f" % (value/100.0,)
1313
1314 @staticmethod
1315 def _convertOBS(value):
1316 """Convert the given OBS value into an integer."""
1317 while value<0.0:
1318 value += 360.0
1319 return int(round(value))
1320
1321 def __init__(self, flapsNotches):
1322 """Construct the aircraft model.
1323
1324 flapsNotches is a list of degrees of flaps that are available on the aircraft."""
1325 self._flapsNotches = flapsNotches
1326 self._simulator = None
1327
1328 @property
1329 def name(self):
1330 """Get the name for this aircraft model."""
1331 return "X-Plane/Generic"
1332
1333 @property
1334 def simulator(self):
1335 """Get the simulator this aircraft model works for."""
1336 return self._simulator
1337
1338 @simulator.setter
1339 def simulator(self, simulator):
1340 """Get the simulator this aircraft model works for."""
1341 self._simulator = simulator
1342
1343 def doesHandle(self, aircraft, aircraftInfo):
1344 """Determine if the model handles the given aircraft name.
1345
1346 This default implementation returns False."""
1347 return False
1348
1349 def _addDatarefWithIndexMember(self, dest, name, type, attrName = None):
1350 """Add the given X-Plane dataref name and type to the given array and a
1351 member attribute with the given name."""
1352 dest.append((name, type))
1353 if attrName is not None:
1354 setattr(self, attrName, len(dest)-1)
1355
1356 def _addDataWithIndexMembers(self, dest, prefix, data):
1357 """Add X-Plane dataref data to the given array and also corresponding
1358 index member variables with the given prefix.
1359
1360 data is a list of triplets of the following items:
1361 - the name of the data item. The index member variable will have a name
1362 created by prepending the given prefix to this name.
1363 - the X-Plane dataref name
1364 - the dataref type
1365
1366 The latter two items will be appended to dest."""
1367 for (name, datarefName, type) in data:
1368 self._addDatarefWithIndexMember(dest, datarefName, type,
1369 prefix + name)
1370
1371 def addMonitoringData(self, data, fsType):
1372 """Add the model-specific monitoring data to the given array."""
1373 self._addDataWithIndexMembers(data, "_monidx_",
1374 AircraftModel.monitoringData)
1375
1376 def getAircraftState(self, aircraft, timestamp, data):
1377 """Get an aircraft state object for the given monitoring data."""
1378 state = fs.AircraftState()
1379
1380 lnavOn = data[self._monidx_autopilotOn]!=0 and \
1381 data[self._monidx_apHeadingMode]==2
1382
1383 state.timestamp = timestamp
1384
1385 state.latitude = data[self._monidx_latitude]
1386 state.longitude = data[self._monidx_longitude]
1387 if state.longitude>180.0: state.longitude = 360.0 - state.longitude
1388
1389 state.paused = data[self._monidx_paused]!=0 or \
1390 data[self._monidx_replay]!=0
1391 state.trickMode = data[self._monidx_replay]!=0
1392
1393 state.overspeed = data[self._monidx_overspeed]!=0
1394 state.stalled = data[self._monidx_stalled]!=0
1395 state.onTheGround = data[self._monidx_onTheGround]!=0
1396
1397 state.zfw = data[self._monidx_emptyWeight] + \
1398 data[self._monidx_payloadWeight]
1399 state.grossWeight = data[self._monidx_grossWeight]
1400
1401 state.heading = data[self._monidx_heading]
1402
1403 state.pitch = -1.0 * data[self._monidx_pitch]
1404 state.bank = data[self._monidx_bank]
1405
1406 state.ias = data[self._monidx_ias]
1407 state.mach = data[self._monidx_mach]
1408 state.groundSpeed = data[self._monidx_groundSpeed] * _mps2knots
1409 state.vs = data[self._monidx_vs]
1410
1411 state.radioAltitude = data[self._monidx_radioAltitude]/.3048
1412 state.altitude = data[self._monidx_altitude]/.3048
1413
1414 state.gLoad = data[self._monidx_gLoad]
1415
1416 flapsControl = data[self._monidx_flapsControl]
1417 flapsIndex = int(flapsControl * (len(self._flapsNotches)-1))
1418 state.flapsSet = 0 if flapsIndex<1 else self._flapsNotches[flapsIndex]
1419
1420 state.flaps = self._flapsNotches[-1]*data[self._monidx_flapsLeft]
1421
1422 state.navLightsOn = data[self._monidx_navLights] != 0
1423 state.antiCollisionLightsOn = data[self._monidx_beaconLights] != 0
1424 state.landingLightsOn = data[self._monidx_landingLights] != 0
1425 state.strobeLightsOn = data[self._monidx_strobeLights] != 0
1426
1427 state.pitotHeatOn = data[self._monidx_pitot]!=0
1428
1429 state.parking = data[self._monidx_parking]>=0.5
1430
1431 state.gearControlDown = data[self._monidx_gearControl]!=0
1432 state.gearsDown = data[self._monidx_noseGear][0]>0.99
1433
1434 state.spoilersArmed = None
1435
1436 state.spoilersExtension = data[self._monidx_spoilers]*100.0
1437
1438 state.altimeter = data[self._monidx_altimeter]* _hgin2hpa
1439 state.altimeterReliable = True
1440 state.qnh = data[self._monidx_qnh]/100.0
1441
1442 state.ils = None
1443 state.ils_obs = None
1444 state.ils_manual = False
1445 state.nav1 = self._convertFrequency(data[self._monidx_nav1])
1446 state.nav1_obs = self._convertOBS(data[self._monidx_nav1_obs])
1447 state.nav1_manual = True
1448 state.nav2 = self._convertFrequency(data[self._monidx_nav2])
1449 state.nav2_obs = self._convertOBS(data[self._monidx_nav2_obs])
1450 state.nav2_manual = not lnavOn
1451 state.adf1 = str(data[self._monidx_adf1])
1452 state.adf2 = str(data[self._monidx_adf2])
1453
1454 state.squawk = "%04d" % (data[self._monidx_squawk],)
1455
1456 state.windSpeed = data[self._monidx_windSpeed]
1457 state.windDirection = data[self._monidx_windDirection]
1458 if state.windDirection<0.0: state.windDirection += 360.0
1459
1460 state.visibility = data[self._monidx_visibility]
1461
1462 state.cog = data[self._monidx_cog]
1463
1464 state.xpdrC = data[self._monidx_xpdrC]==2
1465 state.autoXPDR = False
1466
1467 state.apMaster = data[self._monidx_apMaster]==2
1468 apState = data[self._monidx_apState]
1469 if lnavOn:
1470 state.apHeadingHold = None
1471 state.apHeading = None
1472 else:
1473 state.apHeadingHold = (apState&0x00002)!=0
1474 state.apHeading = data[self._monidx_apHeading]
1475
1476 state.apAltitudeHold = (apState&0x04000)!=0
1477 state.apAltitude = data[self._monidx_apAltitude]
1478
1479 state.elevatorTrim = data[self._monidx_elevatorTrim] * 180.0 / math.pi
1480
1481 state.antiIceOn = data[self._monidx_antiIceOn]!=0 or \
1482 data[self._monidx_surfaceHeat]!=0 or \
1483 data[self._monidx_propHeat]!=0
1484
1485 return state
1486
1487#------------------------------------------------------------------------------
1488
1489class GenericAircraftModel(AircraftModel):
1490 """A generic aircraft model that can handle the fuel levels, the N1 or RPM
1491 values and some other common parameters in a generic way."""
1492
1493 def __init__(self, flapsNotches, fuelTanks, numEngines, isN1 = True):
1494 """Construct the generic aircraft model with the given data.
1495
1496 flapsNotches is an array of how much degrees the individual flaps
1497 notches mean.
1498
1499 fuelTanks is an array of const.FUELTANK_XXX constants about the
1500 aircraft's fuel tanks. They will be converted to offsets.
1501
1502 numEngines is the number of engines the aircraft has.
1503
1504 isN1 determines if the engines have an N1 value or an RPM value
1505 (e.g. pistons)."""
1506 super(GenericAircraftModel, self).__init__(flapsNotches = flapsNotches)
1507
1508 self._fuelTanks = fuelTanks
1509 self._fuelTankCapacities = [1.0] * len(fuelTanks)
1510 self._fuelIndex = None
1511 self._numEngines = numEngines
1512 self._engineStartIndex = None
1513 self._isN1 = isN1
1514
1515 def doesHandle(self, aircraft, aircraftInfo):
1516 """Determine if the model handles the given aircraft name.
1517
1518 This implementation returns True."""
1519 return True
1520
1521 def addMonitoringData(self, data, fsType):
1522 """Add the model-specific monitoring data to the given array."""
1523 super(GenericAircraftModel, self).addMonitoringData(data, fsType)
1524
1525 self._fuelIndex = self._addFuelData(data)
1526
1527 self._engineStartIndex = len(data)
1528 if self._isN1:
1529 self._addDatarefWithIndexMember(data,
1530 "sim/flightmodel/engine/ENGN_N1_",
1531 (TYPE_FLOAT_ARRAY,
1532 self._numEngines))
1533 else:
1534 self._addDatarefWithIndexMember(data,
1535 "sim/flightmodel/engine/POINT_tacrad",
1536 (TYPE_FLOAT_ARRAY,
1537 self._numEngines))
1538
1539 self._addDatarefWithIndexMember(data,
1540 "sim/flightmodel/engine/ENGN_propmode",
1541 (TYPE_INT_ARRAY, self._numEngines))
1542
1543 def getAircraftState(self, aircraft, timestamp, data):
1544 """Get the aircraft state.
1545
1546 Get it from the parent, and then add the data about the fuel levels and
1547 the engine parameters."""
1548 state = super(GenericAircraftModel, self).getAircraftState(aircraft,
1549 timestamp,
1550 data)
1551
1552 state.fuel = []
1553 state.totalFuel = 0.0
1554
1555 fuelAmounts = data[self._fuelIndex]
1556 for i in range(0, len(self._fuelTanks)):
1557 amount = fuelAmounts[i]
1558 state.fuel.append((self._fuelTanks[i], amount))
1559 state.totalFuel += amount
1560
1561 power = data[self._engineStartIndex]
1562
1563 state.n1 = power[:] if self._isN1 else None
1564 state.rpm = None if self._isN1 else power[:]
1565
1566 propMode = data[self._engineStartIndex+1]
1567 state.reverser = [mode == 3 for mode in propMode]
1568
1569 return state
1570
1571 def getFuel(self, handler, callback):
1572 """Get the fuel information for this model.
1573
1574 See Simulator.getFuel for more information. This
1575 implementation simply queries the fuel tanks given to the
1576 constructor."""
1577 data = []
1578 self._addFuelData(data)
1579 data.append( ("sim/aircraft/weight/acf_m_fuel_tot", TYPE_FLOAT) )
1580 data.append( ("sim/aircraft/overflow/acf_tank_rat",
1581 (TYPE_FLOAT_ARRAY, len(self._fuelTanks)) ) )
1582
1583 handler.requestRead(data, self._handleFuelRetrieved,
1584 extra = callback)
1585
1586 def setFuelLevel(self, handler, levels):
1587 """Set the fuel level.
1588
1589 See the description of Simulator.setFuelLevel. This
1590 implementation simply sets the fuel tanks as given."""
1591 data = []
1592 for (tank, level) in levels:
1593 try:
1594 index = self._fuelTanks.index(tank)
1595 data.append( ("sim/flightmodel/weight/m_fuel",
1596 (TYPE_FLOAT_ARRAY, 1, index),
1597 [level * self._fuelTankCapacities[index]]) )
1598 except:
1599 print "xplane.Simulator.setFuelLevel: invalid tank constant: %d" % \
1600 (tank,)
1601
1602 handler.requestWrite(data, self._handleFuelWritten)
1603
1604 def _addFuelData(self, data):
1605 """Add the fuel offsets to the given data array.
1606
1607 Returns the index of the first fuel tank's data."""
1608 fuelStartIndex = len(data)
1609 data.append( ("sim/flightmodel/weight/m_fuel",
1610 (TYPE_FLOAT_ARRAY, len(self._fuelTanks) ) ) )
1611
1612 return fuelStartIndex
1613
1614 def _convertFuelData(self, data, index = 0, addCapacities = False):
1615 """Convert the given data into a fuel info list.
1616
1617 The list consists of two or three-tuples of the following
1618 items:
1619 - the fuel tank ID,
1620 - the amount of the fuel in kg,
1621 - if addCapacities is True, the total capacity of the tank."""
1622 fuelWeight = data[index] / 256.0
1623 index += 1
1624
1625 result = []
1626 totalFuel = 0
1627 for fuelTank in self._fuelTanks:
1628 capacity = data[index+1] * fuelWeight * const.LBSTOKG
1629 if capacity>=1.0:
1630 amount = data[index] * capacity / 128.0 / 65536.0
1631
1632 result.append( (fuelTank, amount, capacity) if addCapacities
1633 else (fuelTank, amount))
1634 totalFuel += amount
1635 index += 2
1636
1637 return (result, totalFuel)
1638
1639 def _handleFuelRetrieved(self, data, callback):
1640 """Callback for a fuel retrieval request."""
1641 result = []
1642 totalCapacity = data[1]
1643 for index in range(0, len(self._fuelTanks)):
1644 amount = data[0][index]
1645 capacity = data[2][index] * totalCapacity
1646 self._fuelTankCapacities[index] = capacity
1647 result.append( (self._fuelTanks[index], amount, capacity) )
1648
1649 callback(result)
1650
1651 def _handleFuelWritten(self, success, extra):
1652 """Callback for a fuel setting request."""
1653 pass
1654
1655#------------------------------------------------------------------------------
1656
1657class GenericModel(GenericAircraftModel):
1658 """Generic aircraft model for an unknown type."""
1659 def __init__(self):
1660 """Construct the model."""
1661 super(GenericModel, self). \
1662 __init__(flapsNotches = [0, 10, 20, 30],
1663 fuelTanks = [const.FUELTANK_LEFT, const.FUELTANK_RIGHT],
1664 numEngines = 2)
1665
1666 @property
1667 def name(self):
1668 """Get the name for this aircraft model."""
1669 return "X-Plane/Generic"
1670
1671#------------------------------------------------------------------------------
1672
1673class B737Model(GenericAircraftModel):
1674 """Generic model for the Boeing 737 Classing and NG aircraft."""
1675 fuelTanks = [const.FUELTANK_LEFT, const.FUELTANK_CENTRE, const.FUELTANK_RIGHT]
1676
1677 def __init__(self):
1678 """Construct the model."""
1679 super(B737Model, self). \
1680 __init__(flapsNotches = [0, 1, 2, 5, 10, 15, 25, 30, 40],
1681 fuelTanks = B737Model.fuelTanks,
1682 numEngines = 2)
1683
1684 @property
1685 def name(self):
1686 """Get the name for this aircraft model."""
1687 return "X-Plane/Generic Boeing 737"
1688
1689#------------------------------------------------------------------------------
1690
1691class B767Model(GenericAircraftModel):
1692 """Generic model for the Boeing 767 aircraft."""
1693 fuelTanks = [const.FUELTANK_LEFT, const.FUELTANK_CENTRE, const.FUELTANK_RIGHT]
1694
1695 def __init__(self):
1696 """Construct the model."""
1697 super(B767Model, self). \
1698 __init__(flapsNotches = [0, 1, 5, 15, 20, 25, 30],
1699 fuelTanks = B767Model.fuelTanks,
1700 numEngines = 2)
1701
1702 @property
1703 def name(self):
1704 """Get the name for this aircraft model."""
1705 return "X-Plane/Generic Boeing 767"
1706
1707#------------------------------------------------------------------------------
1708
1709class DH8DModel(GenericAircraftModel):
1710 """Generic model for the Bombardier Dash 8-Q400 aircraft."""
1711 fuelTanks = [const.FUELTANK_LEFT, const.FUELTANK_RIGHT]
1712
1713 def __init__(self):
1714 """Construct the model."""
1715 super(DH8DModel, self). \
1716 __init__(flapsNotches = [0, 5, 10, 15, 35],
1717 fuelTanks = DH8DModel.fuelTanks,
1718 numEngines = 2)
1719
1720 @property
1721 def name(self):
1722 """Get the name for this aircraft model."""
1723 return "X-Plane/Generic Bombardier Dash 8-Q400"
1724
1725#------------------------------------------------------------------------------
1726
1727class FJSDH8DModel(DH8DModel):
1728 """Model handler for the FlyJSim Dash 8-Q400."""
1729 @staticmethod
1730 def doesHandle(aircraft, (tailnum, author, description, notes,
1731 icao, liveryPath)):
1732 """Determine if this model handler handles the aircraft with the given
1733 name."""
1734 return aircraft.type==const.AIRCRAFT_DH8D and \
1735 description.find("Dash 8 Q400")!=-1 and \
1736 ((author in ["2012", "2013"] and tailnum=="N62890") or \
1737 author.find("Jack Skieczius")!=-1)
1738
1739 @property
1740 def name(self):
1741 """Get the name for this aircraft model."""
1742 return "X-Plane/FlyJSim Bombardier Dash 8-Q400"
1743
1744 def addMonitoringData(self, data, fsType):
1745 """Add the model-specific monitoring data to the given array."""
1746 super(FJSDH8DModel, self).addMonitoringData(data, fsType)
1747
1748 self._speedBrakeIndex = len(data)
1749 self._addDatarefWithIndexMember(data,
1750 "sim/flightmodel2/wing/speedbrake1_deg",
1751 (TYPE_FLOAT_ARRAY, 2))
1752 self._addDatarefWithIndexMember(data,
1753 "sim/flightmodel2/wing/speedbrake2_deg",
1754 (TYPE_FLOAT_ARRAY, 2))
1755
1756
1757 def getAircraftState(self, aircraft, timestamp, data):
1758 """Get the aircraft state.
1759
1760 Get it from the parent, and then invert the pitot heat state."""
1761 state = super(FJSDH8DModel, self).getAircraftState(aircraft,
1762 timestamp,
1763 data)
1764 state.antiCollisionLightsOn = \
1765 state.antiCollisionLightsOn or state.strobeLightsOn
1766 state.cog = (state.cog / 0.0254 + 21.504) / 94.512
1767
1768 # It seems that N1 does not always go down to 0 properly
1769 # (maybe due to winds?)
1770 state.n1 = [0 if n1<2.0 else n1 for n1 in state.n1]
1771
1772 state.spoilersExtension = \
1773 sum(data[self._speedBrakeIndex] + data[self._speedBrakeIndex+1])/4
1774
1775 return state
1776
1777#------------------------------------------------------------------------------
1778
1779class CRJ2Model(GenericAircraftModel):
1780 """Generic model for the Bombardier CRJ-200 aircraft."""
1781 fuelTanks = [const.FUELTANK_LEFT, const.FUELTANK_CENTRE, const.FUELTANK_RIGHT]
1782
1783 def __init__(self):
1784 """Construct the model."""
1785 super(CRJ2Model, self). \
1786 __init__(flapsNotches = [0, 8, 20, 30, 45],
1787 fuelTanks = CRJ2Model.fuelTanks,
1788 numEngines = 2)
1789
1790 @property
1791 def name(self):
1792 """Get the name for this aircraft model."""
1793 return "X-Plane/Generic Bombardier CRJ-200"
1794
1795#------------------------------------------------------------------------------
1796
1797class F70Model(GenericAircraftModel):
1798 """Generic model for the Fokker F70 aircraft."""
1799 fuelTanks = [const.FUELTANK_LEFT, const.FUELTANK_CENTRE, const.FUELTANK_RIGHT]
1800
1801 def __init__(self):
1802 """Construct the model."""
1803 super(F70Model, self). \
1804 __init__(flapsNotches = [0, 8, 15, 25, 42],
1805 fuelTanks = F70Model.fuelTanks,
1806 numEngines = 2)
1807
1808 @property
1809 def name(self):
1810 """Get the name for this aircraft model."""
1811 return "X-Plane/Generic Fokker 70"
1812
1813#------------------------------------------------------------------------------
1814
1815class DC3Model(GenericAircraftModel):
1816 """Generic model for the Lisunov Li-2 (DC-3) aircraft."""
1817 fuelTanks = [const.FUELTANK_LEFT, const.FUELTANK_CENTRE,
1818 const.FUELTANK_RIGHT]
1819 # fuelTanks = [const.FUELTANK_LEFT_AUX, const.FUELTANK_LEFT,
1820 # const.FUELTANK_RIGHT, const.FUELTANK_RIGHT_AUX]
1821
1822 def __init__(self):
1823 """Construct the model."""
1824 super(DC3Model, self). \
1825 __init__(flapsNotches = [0, 15, 30, 45],
1826 fuelTanks = DC3Model.fuelTanks,
1827 numEngines = 2, isN1 = False)
1828 self._leftLevel = 0.0
1829 self._rightLevel = 0.0
1830
1831 @property
1832 def name(self):
1833 """Get the name for this aircraft model."""
1834 return "X-Plane/Generic Lisunov Li-2 (DC-3)"
1835
1836#------------------------------------------------------------------------------
1837
1838class T134Model(GenericAircraftModel):
1839 """Generic model for the Tupolev Tu-134 aircraft."""
1840 fuelTanks = [const.FUELTANK_LEFT_TIP, const.FUELTANK_EXTERNAL1,
1841 const.FUELTANK_LEFT_AUX,
1842 const.FUELTANK_CENTRE,
1843 const.FUELTANK_RIGHT_AUX,
1844 const.FUELTANK_EXTERNAL2, const.FUELTANK_RIGHT_TIP]
1845
1846 def __init__(self):
1847 """Construct the model."""
1848 super(T134Model, self). \
1849 __init__(flapsNotches = [0, 10, 20, 30],
1850 fuelTanks = T134Model.fuelTanks,
1851 numEngines = 2)
1852
1853 @property
1854 def name(self):
1855 """Get the name for this aircraft model."""
1856 return "X-Plane/Generic Tupolev Tu-134"
1857
1858#------------------------------------------------------------------------------
1859
1860class T154Model(GenericAircraftModel):
1861 """Generic model for the Tupolev Tu-134 aircraft."""
1862 fuelTanks = [const.FUELTANK_LEFT_AUX, const.FUELTANK_LEFT,
1863 const.FUELTANK_CENTRE, const.FUELTANK_CENTRE2,
1864 const.FUELTANK_RIGHT, const.FUELTANK_RIGHT_AUX]
1865
1866 def __init__(self):
1867 """Construct the model."""
1868 super(T154Model, self). \
1869 __init__(flapsNotches = [0, 15, 28, 45],
1870 fuelTanks = T154Model.fuelTanks,
1871 numEngines = 3)
1872
1873 @property
1874 def name(self):
1875 """Get the name for this aircraft model."""
1876 return "X-Plane/Generic Tupolev Tu-154"
1877
1878 def getAircraftState(self, aircraft, timestamp, data):
1879 """Get an aircraft state object for the given monitoring data.
1880
1881 This removes the reverser value for the middle engine."""
1882 state = super(T154Model, self).getAircraftState(aircraft, timestamp, data)
1883 del state.reverser[1]
1884 return state
1885
1886#------------------------------------------------------------------------------
1887
1888class YK40Model(GenericAircraftModel):
1889 """Generic model for the Yakovlev Yak-40 aircraft."""
1890 fuelTanks = [const.FUELTANK_LEFT, const.FUELTANK_RIGHT]
1891
1892 def __init__(self):
1893 """Construct the model."""
1894 super(YK40Model, self). \
1895 __init__(flapsNotches = [0, 20, 35],
1896 fuelTanks = YK40Model.fuelTanks,
1897 numEngines = 2)
1898
1899 @property
1900 def name(self):
1901 """Get the name for this aircraft model."""
1902 return "X-Plane/Generic Yakovlev Yak-40"
1903
1904#------------------------------------------------------------------------------
1905
1906_genericModels = { const.AIRCRAFT_B736 : B737Model,
1907 const.AIRCRAFT_B737 : B737Model,
1908 const.AIRCRAFT_B738 : B737Model,
1909 const.AIRCRAFT_B738C : B737Model,
1910 const.AIRCRAFT_B732 : B737Model,
1911 const.AIRCRAFT_B733 : B737Model,
1912 const.AIRCRAFT_B734 : B737Model,
1913 const.AIRCRAFT_B735 : B737Model,
1914 const.AIRCRAFT_DH8D : DH8DModel,
1915 const.AIRCRAFT_B762 : B767Model,
1916 const.AIRCRAFT_B763 : B767Model,
1917 const.AIRCRAFT_CRJ2 : CRJ2Model,
1918 const.AIRCRAFT_F70 : F70Model,
1919 const.AIRCRAFT_DC3 : DC3Model,
1920 const.AIRCRAFT_T134 : T134Model,
1921 const.AIRCRAFT_T154 : T154Model,
1922 const.AIRCRAFT_YK40 : YK40Model }
1923
1924#------------------------------------------------------------------------------
1925
1926AircraftModel.registerSpecial(FJSDH8DModel)
1927
1928#------------------------------------------------------------------------------
1929
1930# if __name__ == "__main__":
1931# class ConnectionListener:
1932# def connected(self, fsType, descriptor):
1933# """Called when a connection has been established to the flight
1934# simulator of the given type."""
1935# print "fs.ConnectionListener.connected, fsType:", fsType, ", descriptor:", descriptor
1936
1937# def connectionFailed(self):
1938# """Called when the connection could not be established."""
1939# print "fs.ConnectionListener.connectionFailed"
1940
1941# def disconnected(self):
1942# """Called when a connection to the flight simulator has been broken."""
1943# print "fs.ConnectionListener.disconnected"
1944
1945# class Config:
1946# def __init__(self):
1947# self.onlineACARS = False
1948# self.realIASSmoothingLength = 2
1949# self.realVSSmoothingLength = 2
1950# self.enableSounds = False
1951# self.usingFS2Crew = False
1952
1953# def isMessageTypeFS(self, type):
1954# return True
1955
1956
1957
1958# class GUI:
1959# def __init__(self):
1960# self.config = Config()
1961# self.entranceExam = False
1962# self.zfw = 30000.0
1963
1964# def resetFlightStatus(self):
1965# pass
1966
1967# def setRating(self, value):
1968# pass
1969
1970# def insertFlightLogLine(self, index, ts, text, isFault):
1971# pass
1972
1973# def setStage(self, stage):
1974# pass
1975
1976
1977# from i18n import setLanguage
1978
1979# setLanguage("/home/vi/munka/repules/mlx", "en")
1980
1981# from logger import Logger
1982# from flight import Flight
1983# from acft import DH8D
1984
1985# gui = GUI()
1986
1987# logger = Logger(gui)
1988
1989# flight = Flight(logger, gui)
1990# acft = DH8D(flight)
1991
1992# Watchdog()
1993
1994# connectionListener = ConnectionListener()
1995# simulator = Simulator(connectionListener, connectAttempts = 3)
1996
1997# simulator.connect(acft)
1998
1999# time.sleep(2)
2000
2001# simulator.startMonitoring()
2002
2003# simulator.sendMessage("[MLX] Flight stage: Taxi", duration = 3)
2004
2005# time.sleep(4)
2006
2007# 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)
2008# #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)
2009# #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)
2010
2011# time.sleep(30)
2012
2013# simulator.sendMessage("[MLX] Hello", duration = 3)
2014
2015# time.sleep(10)
Note: See TracBrowser for help on using the repository browser.