source: src/mlx/xplane.py@ 421:374788cb36e5

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

Added support for implementing the message display in the simulator

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