source: src/mlx/fsuipc.py@ 212:fab302d5b7f6

Last change on this file since 212:fab302d5b7f6 was 212:fab302d5b7f6, checked in by István Váradi <ivaradi@…>, 12 years ago

Added support for the lights switches of the PMDG 737 NGX

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