source: src/mlx/xplane.py@ 420:fc063b7b571e

xplane
Last change on this file since 420:fc063b7b571e was 420:fc063b7b571e, checked in by István Váradi <ivaradi@…>, 11 years ago

Implemented the most important parts of X-Plane support

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