source: src/mlx/fsuipc.py@ 209:3172e5cab96e

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

The position of the gear control lever is logged

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