source: src/mlx/fsuipc.py@ 180:a126eca82a37

Last change on this file since 180:a126eca82a37 was 168:71af690e0c26, checked in by István Váradi <ivaradi@…>, 12 years ago

The hotkey handling works

File size: 63.7 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 ("noseGear", 0x0bec, "d"),
1148 ("spoilersArmed", 0x0bcc, "d"),
1149 ("spoilers", 0x0bd0, "d"),
1150 ("altimeter", 0x0330, "H"),
1151 ("nav1", 0x0350, "H"),
1152 ("nav2", 0x0352, "H"),
1153 ("squawk", 0x0354, "H"),
1154 ("windSpeed", 0x0e90, "H"),
1155 ("windDirection", 0x0e92, "H"),
1156 ("visibility", 0x0e8a, "H")]
1157
1158
1159 specialModels = []
1160
1161 @staticmethod
1162 def registerSpecial(clazz):
1163 """Register the given class as a special model."""
1164 AircraftModel.specialModels.append(clazz)
1165
1166 @staticmethod
1167 def findSpecial(aircraft, aircraftName):
1168 for specialModel in AircraftModel.specialModels:
1169 if specialModel.doesHandle(aircraft, aircraftName):
1170 return specialModel
1171 return None
1172
1173 @staticmethod
1174 def create(aircraft, aircraftName):
1175 """Create the model for the given aircraft name, and notify the
1176 aircraft about it."""
1177 specialModel = AircraftModel.findSpecial(aircraft, aircraftName)
1178 if specialModel is not None:
1179 return specialModel()
1180 if aircraft.type in _genericModels:
1181 return _genericModels[aircraft.type]()
1182 else:
1183 return GenericModel()
1184
1185 @staticmethod
1186 def convertBCD(data, length):
1187 """Convert a data item encoded as BCD into a string of the given number
1188 of digits."""
1189 bcd = ""
1190 for i in range(0, length):
1191 digit = chr(ord('0') + (data&0x0f))
1192 data >>= 4
1193 bcd = digit + bcd
1194 return bcd
1195
1196 @staticmethod
1197 def convertFrequency(data):
1198 """Convert the given frequency data to a string."""
1199 bcd = AircraftModel.convertBCD(data, 4)
1200 return "1" + bcd[0:2] + "." + bcd[2:4]
1201
1202 def __init__(self, flapsNotches):
1203 """Construct the aircraft model.
1204
1205 flapsNotches is a list of degrees of flaps that are available on the aircraft."""
1206 self._flapsNotches = flapsNotches
1207
1208 @property
1209 def name(self):
1210 """Get the name for this aircraft model."""
1211 return "FSUIPC/Generic"
1212
1213 def doesHandle(self, aircraft, aircraftName):
1214 """Determine if the model handles the given aircraft name.
1215
1216 This default implementation returns False."""
1217 return False
1218
1219 def _addOffsetWithIndexMember(self, dest, offset, type, attrName = None):
1220 """Add the given FSUIPC offset and type to the given array and a member
1221 attribute with the given name."""
1222 dest.append((offset, type))
1223 if attrName is not None:
1224 setattr(self, attrName, len(dest)-1)
1225
1226 def _addDataWithIndexMembers(self, dest, prefix, data):
1227 """Add FSUIPC data to the given array and also corresponding index
1228 member variables with the given prefix.
1229
1230 data is a list of triplets of the following items:
1231 - the name of the data item. The index member variable will have a name
1232 created by prepending the given prefix to this name.
1233 - the FSUIPC offset
1234 - the FSUIPC type
1235
1236 The latter two items will be appended to dest."""
1237 for (name, offset, type) in data:
1238 self._addOffsetWithIndexMember(dest, offset, type, prefix + name)
1239
1240 def addMonitoringData(self, data):
1241 """Add the model-specific monitoring data to the given array."""
1242 self._addDataWithIndexMembers(data, "_monidx_",
1243 AircraftModel.monitoringData)
1244
1245 def getAircraftState(self, aircraft, timestamp, data):
1246 """Get an aircraft state object for the given monitoring data."""
1247 state = fs.AircraftState()
1248
1249 state.timestamp = timestamp
1250
1251 state.latitude = data[self._monidx_latitude] * \
1252 90.0 / 10001750.0 / 65536.0 / 65536.0
1253
1254 state.longitude = data[self._monidx_longitude] * \
1255 360.0 / 65536.0 / 65536.0 / 65536.0 / 65536.0
1256 if state.longitude>180.0: state.longitude = 360.0 - state.longitude
1257
1258 state.paused = data[self._monidx_paused]!=0 or \
1259 data[self._monidx_frozen]!=0 or \
1260 data[self._monidx_replay]!=0
1261 state.trickMode = data[self._monidx_slew]!=0
1262
1263 state.overspeed = data[self._monidx_overspeed]!=0
1264 state.stalled = data[self._monidx_stalled]!=0
1265 state.onTheGround = data[self._monidx_onTheGround]!=0
1266
1267 state.zfw = data[self._monidx_zfw] * const.LBSTOKG / 256.0
1268 state.grossWeight = data[self._monidx_grossWeight] * const.LBSTOKG
1269
1270 state.heading = Handler.fsuipc2PositiveDegrees(data[self._monidx_heading])
1271
1272 state.pitch = Handler.fsuipc2Degrees(data[self._monidx_pitch])
1273 state.bank = Handler.fsuipc2Degrees(data[self._monidx_bank])
1274
1275 state.ias = Handler.fsuipc2IAS(data[self._monidx_ias])
1276 state.mach = data[self._monidx_mach] / 20480.0
1277 state.groundSpeed = data[self._monidx_groundSpeed]* 3600.0/65536.0/1852.0
1278 state.vs = Handler.fsuipc2VS(data[self._monidx_vs])
1279
1280 state.radioAltitude = \
1281 Handler.fsuipc2radioAltitude(data[self._monidx_radioAltitude])
1282 state.altitude = data[self._monidx_altitude]/const.FEETTOMETRES/65536.0/65536.0
1283
1284 state.gLoad = data[self._monidx_gLoad] / 625.0
1285
1286 numNotchesM1 = len(self._flapsNotches) - 1
1287 flapsIncrement = 16383 / numNotchesM1
1288 flapsControl = data[self._monidx_flapsControl]
1289 flapsIndex = flapsControl / flapsIncrement
1290 if flapsIndex < numNotchesM1:
1291 if (flapsControl - (flapsIndex*flapsIncrement) >
1292 (flapsIndex+1)*flapsIncrement - flapsControl):
1293 flapsIndex += 1
1294 state.flapsSet = self._flapsNotches[flapsIndex]
1295
1296 flapsLeft = data[self._monidx_flapsLeft]
1297 state.flaps = self._flapsNotches[-1]*flapsLeft/16383.0
1298
1299 lights = data[self._monidx_lights]
1300
1301 state.navLightsOn = (lights&0x01) != 0
1302 state.antiCollisionLightsOn = (lights&0x02) != 0
1303 state.landingLightsOn = (lights&0x04) != 0
1304 state.strobeLightsOn = (lights&0x10) != 0
1305
1306 state.pitotHeatOn = data[self._monidx_pitot]!=0
1307
1308 state.parking = data[self._monidx_parking]!=0
1309
1310 state.gearsDown = data[self._monidx_noseGear]==16383
1311
1312 state.spoilersArmed = data[self._monidx_spoilersArmed]!=0
1313
1314 spoilers = data[self._monidx_spoilers]
1315 if spoilers<=4800:
1316 state.spoilersExtension = 0.0
1317 else:
1318 state.spoilersExtension = (spoilers - 4800) * 100.0 / (16383 - 4800)
1319
1320 state.altimeter = data[self._monidx_altimeter] / 16.0
1321
1322 state.nav1 = AircraftModel.convertFrequency(data[self._monidx_nav1])
1323 state.nav2 = AircraftModel.convertFrequency(data[self._monidx_nav2])
1324 state.squawk = AircraftModel.convertBCD(data[self._monidx_squawk], 4)
1325
1326 state.windSpeed = data[self._monidx_windSpeed]
1327 state.windDirection = data[self._monidx_windDirection]*360.0/65536.0
1328 if state.windDirection<0.0: state.windDirection += 360.0
1329
1330 state.visibility = data[self._monidx_visibility]*1609.344/100.0
1331
1332 return state
1333
1334#------------------------------------------------------------------------------
1335
1336class GenericAircraftModel(AircraftModel):
1337 """A generic aircraft model that can handle the fuel levels, the N1 or RPM
1338 values and some other common parameters in a generic way."""
1339
1340 def __init__(self, flapsNotches, fuelTanks, numEngines, isN1 = True):
1341 """Construct the generic aircraft model with the given data.
1342
1343 flapsNotches is an array of how much degrees the individual flaps
1344 notches mean.
1345
1346 fuelTanks is an array of const.FUELTANK_XXX constants about the
1347 aircraft's fuel tanks. They will be converted to offsets.
1348
1349 numEngines is the number of engines the aircraft has.
1350
1351 isN1 determines if the engines have an N1 value or an RPM value
1352 (e.g. pistons)."""
1353 super(GenericAircraftModel, self).__init__(flapsNotches = flapsNotches)
1354
1355 self._fuelTanks = fuelTanks
1356 self._fuelStartIndex = None
1357 self._numEngines = numEngines
1358 self._engineStartIndex = None
1359 self._isN1 = isN1
1360
1361 def doesHandle(self, aircraft, aircraftName):
1362 """Determine if the model handles the given aircraft name.
1363
1364 This implementation returns True."""
1365 return True
1366
1367 def addMonitoringData(self, data):
1368 """Add the model-specific monitoring data to the given array."""
1369 super(GenericAircraftModel, self).addMonitoringData(data)
1370
1371 self._addOffsetWithIndexMember(data, 0x0af4, "H", "_monidx_fuelWeight")
1372
1373 self._fuelStartIndex = len(data)
1374 for tank in self._fuelTanks:
1375 offset = _tank2offset[tank]
1376 self._addOffsetWithIndexMember(data, offset, "u") # tank level
1377 self._addOffsetWithIndexMember(data, offset+4, "u") # tank capacity
1378
1379 if self._isN1:
1380 self._engineStartIndex = len(data)
1381 for i in range(0, self._numEngines):
1382 self._addOffsetWithIndexMember(data, 0x2000 + i * 0x100, "f") # N1
1383 self._addOffsetWithIndexMember(data, 0x088c + i * 0x98, "h") # throttle lever
1384
1385 def getAircraftState(self, aircraft, timestamp, data):
1386 """Get the aircraft state.
1387
1388 Get it from the parent, and then add the data about the fuel levels and
1389 the engine parameters."""
1390 state = super(GenericAircraftModel, self).getAircraftState(aircraft,
1391 timestamp,
1392 data)
1393
1394 fuelWeight = data[self._monidx_fuelWeight]/256.0
1395 state.fuel = []
1396 for i in range(self._fuelStartIndex,
1397 self._fuelStartIndex + 2*len(self._fuelTanks), 2):
1398 fuel = data[i+1]*data[i]*fuelWeight*const.LBSTOKG/128.0/65536.0
1399 state.fuel.append(fuel)
1400
1401 state.n1 = []
1402 state.reverser = []
1403 for i in range(self._engineStartIndex,
1404 self._engineStartIndex + 2*self._numEngines, 2):
1405 state.n1.append(data[i])
1406 state.reverser.append(data[i+1]<0)
1407
1408 return state
1409
1410#------------------------------------------------------------------------------
1411
1412class GenericModel(GenericAircraftModel):
1413 """Generic aircraft model for an unknown type."""
1414 def __init__(self):
1415 """Construct the model."""
1416 super(GenericModel, self). \
1417 __init__(flapsNotches = [0, 10, 20, 30],
1418 fuelTanks = [const.FUELTANK_LEFT, const.FUELTANK_RIGHT],
1419 numEngines = 2)
1420
1421 @property
1422 def name(self):
1423 """Get the name for this aircraft model."""
1424 return "FSUIPC/Generic"
1425
1426#------------------------------------------------------------------------------
1427
1428class B737Model(GenericAircraftModel):
1429 """Generic model for the Boeing 737 Classing and NG aircraft."""
1430 def __init__(self):
1431 """Construct the model."""
1432 super(B737Model, self). \
1433 __init__(flapsNotches = [0, 1, 2, 5, 10, 15, 25, 30, 40],
1434 fuelTanks = acft.Boeing737.fuelTanks,
1435 numEngines = 2)
1436
1437 @property
1438 def name(self):
1439 """Get the name for this aircraft model."""
1440 return "FSUIPC/Generic Boeing 737"
1441
1442#------------------------------------------------------------------------------
1443
1444class PMDGBoeing737NGModel(B737Model):
1445 """A model handler for the PMDG Boeing 737NG model."""
1446 @staticmethod
1447 def doesHandle(aircraft, (name, airPath)):
1448 """Determine if this model handler handles the aircraft with the given
1449 name."""
1450 return aircraft.type in [const.AIRCRAFT_B736,
1451 const.AIRCRAFT_B737,
1452 const.AIRCRAFT_B738] and \
1453 (name.find("PMDG")!=-1 or airPath.find("PMDG")!=-1) and \
1454 (name.find("737")!=-1 or airPath.find("737")!=-1) and \
1455 (name.find("600")!=-1 or airPath.find("600")!=-1 or \
1456 name.find("700")!=-1 or airPath.find("700")!=-1 or \
1457 name.find("800")!=-1 or airPath.find("800")!=-1 or \
1458 name.find("900")!=-1 or airPath.find("900")!=-1)
1459
1460 @property
1461 def name(self):
1462 """Get the name for this aircraft model."""
1463 return "FSUIPC/PMDG Boeing 737NG"
1464
1465 def addMonitoringData(self, data):
1466 """Add the model-specific monitoring data to the given array."""
1467 super(PMDGBoeing737NGModel, self).addMonitoringData(data)
1468
1469 self._addOffsetWithIndexMember(data, 0x6202, "b", "_pmdgidx_switches")
1470
1471 def getAircraftState(self, aircraft, timestamp, data):
1472 """Get the aircraft state.
1473
1474 Get it from the parent, and then check some PMDG-specific stuff."""
1475 state = super(PMDGBoeing737NGModel, self).getAircraftState(aircraft,
1476 timestamp,
1477 data)
1478 if data[self._pmdgidx_switches]&0x01==0x01:
1479 state.altimeter = 1013.25
1480
1481 return state
1482
1483#------------------------------------------------------------------------------
1484
1485class B767Model(GenericAircraftModel):
1486 """Generic model for the Boeing 767 aircraft."""
1487 def __init__(self):
1488 """Construct the model."""
1489 super(B767Model, self). \
1490 __init__(flapsNotches = [0, 1, 5, 15, 20, 25, 30],
1491 fuelTanks = acft.Boeing767.fuelTanks,
1492 numEngines = 2)
1493
1494 @property
1495 def name(self):
1496 """Get the name for this aircraft model."""
1497 return "FSUIPC/Generic Boeing 767"
1498
1499#------------------------------------------------------------------------------
1500
1501class DH8DModel(GenericAircraftModel):
1502 """Generic model for the Bombardier Dash 8-Q400 aircraft."""
1503 def __init__(self):
1504 """Construct the model."""
1505 super(DH8DModel, self). \
1506 __init__(flapsNotches = [0, 5, 10, 15, 35],
1507 fuelTanks = acft.DH8D.fuelTanks,
1508 numEngines = 2)
1509
1510 @property
1511 def name(self):
1512 """Get the name for this aircraft model."""
1513 return "FSUIPC/Generic Bombardier Dash 8-Q400"
1514
1515#------------------------------------------------------------------------------
1516
1517class DreamwingsDH8DModel(DH8DModel):
1518 """Model handler for the Dreamwings Dash 8-Q400."""
1519 @staticmethod
1520 def doesHandle(aircraft, (name, airPath)):
1521 """Determine if this model handler handles the aircraft with the given
1522 name."""
1523 return aircraft.type==const.AIRCRAFT_DH8D and \
1524 (name.find("Dreamwings")!=-1 or airPath.find("Dreamwings")!=-1) and \
1525 (name.find("Dash")!=-1 or airPath.find("Dash")!=-1) and \
1526 (name.find("Q400")!=-1 or airPath.find("Q400")!=-1) and \
1527 airPath.find("Dash8Q400")!=-1
1528
1529 @property
1530 def name(self):
1531 """Get the name for this aircraft model."""
1532 return "FSUIPC/Dreamwings Bombardier Dash 8-Q400"
1533
1534 def getAircraftState(self, aircraft, timestamp, data):
1535 """Get the aircraft state.
1536
1537 Get it from the parent, and then invert the pitot heat state."""
1538 state = super(DreamwingsDH8DModel, self).getAircraftState(aircraft,
1539 timestamp,
1540 data)
1541 state.pitotHeatOn = not state.pitotHeatOn
1542
1543 return state
1544#------------------------------------------------------------------------------
1545
1546class CRJ2Model(GenericAircraftModel):
1547 """Generic model for the Bombardier CRJ-200 aircraft."""
1548 def __init__(self):
1549 """Construct the model."""
1550 super(CRJ2Model, self). \
1551 __init__(flapsNotches = [0, 8, 20, 30, 45],
1552 fuelTanks = acft.CRJ2.fuelTanks,
1553 numEngines = 2)
1554
1555 @property
1556 def name(self):
1557 """Get the name for this aircraft model."""
1558 return "FSUIPC/Generic Bombardier CRJ-200"
1559
1560#------------------------------------------------------------------------------
1561
1562class F70Model(GenericAircraftModel):
1563 """Generic model for the Fokker F70 aircraft."""
1564 def __init__(self):
1565 """Construct the model."""
1566 super(F70Model, self). \
1567 __init__(flapsNotches = [0, 8, 15, 25, 42],
1568 fuelTanks = acft.F70.fuelTanks,
1569 numEngines = 2)
1570
1571 @property
1572 def name(self):
1573 """Get the name for this aircraft model."""
1574 return "FSUIPC/Generic Fokker 70"
1575
1576#------------------------------------------------------------------------------
1577
1578class DC3Model(GenericAircraftModel):
1579 """Generic model for the Lisunov Li-2 (DC-3) aircraft."""
1580 def __init__(self):
1581 """Construct the model."""
1582 super(DC3Model, self). \
1583 __init__(flapsNotches = [0, 15, 30, 45],
1584 fuelTanks = acft.DC3.fuelTanks,
1585 numEngines = 2)
1586
1587 @property
1588 def name(self):
1589 """Get the name for this aircraft model."""
1590 return "FSUIPC/Generic Lisunov Li-2"
1591
1592#------------------------------------------------------------------------------
1593
1594class T134Model(GenericAircraftModel):
1595 """Generic model for the Tupolev Tu-134 aircraft."""
1596 def __init__(self):
1597 """Construct the model."""
1598 super(T134Model, self). \
1599 __init__(flapsNotches = [0, 10, 20, 30],
1600 fuelTanks = acft.T134.fuelTanks,
1601 numEngines = 2)
1602
1603 @property
1604 def name(self):
1605 """Get the name for this aircraft model."""
1606 return "FSUIPC/Generic Tupolev Tu-134"
1607
1608#------------------------------------------------------------------------------
1609
1610class T154Model(GenericAircraftModel):
1611 """Generic model for the Tupolev Tu-134 aircraft."""
1612 def __init__(self):
1613 """Construct the model."""
1614 super(T154Model, self). \
1615 __init__(flapsNotches = [0, 15, 28, 45],
1616 fuelTanks = acft.T154.fuelTanks,
1617 numEngines = 3)
1618
1619 @property
1620 def name(self):
1621 """Get the name for this aircraft model."""
1622 return "FSUIPC/Generic Tupolev Tu-154"
1623
1624 def getAircraftState(self, aircraft, timestamp, data):
1625 """Get an aircraft state object for the given monitoring data.
1626
1627 This removes the reverser value for the middle engine."""
1628 state = super(T154Model, self).getAircraftState(aircraft, timestamp, data)
1629 del state.reverser[1]
1630 return state
1631
1632#------------------------------------------------------------------------------
1633
1634class YK40Model(GenericAircraftModel):
1635 """Generic model for the Yakovlev Yak-40 aircraft."""
1636 def __init__(self):
1637 """Construct the model."""
1638 super(YK40Model, self). \
1639 __init__(flapsNotches = [0, 20, 35],
1640 fuelTanks = acft.YK40.fuelTanks,
1641 numEngines = 2)
1642
1643 @property
1644 def name(self):
1645 """Get the name for this aircraft model."""
1646 return "FSUIPC/Generic Yakovlev Yak-40"
1647
1648#------------------------------------------------------------------------------
1649
1650_genericModels = { const.AIRCRAFT_B736 : B737Model,
1651 const.AIRCRAFT_B737 : B737Model,
1652 const.AIRCRAFT_B738 : B737Model,
1653 const.AIRCRAFT_B733 : B737Model,
1654 const.AIRCRAFT_B734 : B737Model,
1655 const.AIRCRAFT_B735 : B737Model,
1656 const.AIRCRAFT_DH8D : DH8DModel,
1657 const.AIRCRAFT_B762 : B767Model,
1658 const.AIRCRAFT_B763 : B767Model,
1659 const.AIRCRAFT_CRJ2 : B767Model,
1660 const.AIRCRAFT_F70 : F70Model,
1661 const.AIRCRAFT_DC3 : DC3Model,
1662 const.AIRCRAFT_T134 : T134Model,
1663 const.AIRCRAFT_T154 : T154Model,
1664 const.AIRCRAFT_YK40 : YK40Model }
1665
1666#------------------------------------------------------------------------------
1667
1668AircraftModel.registerSpecial(PMDGBoeing737NGModel)
1669AircraftModel.registerSpecial(DreamwingsDH8DModel)
1670
1671#------------------------------------------------------------------------------
Note: See TracBrowser for help on using the repository browser.