source: src/mlx/fsuipc.py@ 244:afe588c772a0

Last change on this file since 244:afe588c772a0 was 243:1a42c5aa468b, checked in by István Váradi <ivaradi@…>, 12 years ago

Added the logging of the CoG value

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