source: src/mlx/fsuipc.py@ 291:879f2d24fe4c

Last change on this file since 291:879f2d24fe4c was 290:1435334517fc, checked in by István Váradi <ivaradi@…>, 12 years ago

Fixed problem with querying the fuel tank info too early

File size: 71.2 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()
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 self._fuelCallback = None
574
575 def connect(self, aircraft):
576 """Initiate a connection to the simulator."""
577 self._aircraft = aircraft
578 self._aircraftName = None
579 self._aircraftModel = None
580 self._handler.connect()
581 if self._normalRequestID is None:
582 self._nextSyncTime = -1
583 self._startDefaultNormal()
584
585 def reconnect(self):
586 """Initiate a reconnection to the simulator.
587
588 It does not reset already set up data, just calls connect() on the
589 handler."""
590 self._handler.connect()
591
592 def requestZFW(self, callback):
593 """Send a request for the ZFW."""
594 self._handler.requestRead([(0x3bfc, "d")], self._handleZFW, extra = callback)
595
596 def requestWeights(self, callback):
597 """Request the following weights: DOW, ZFW, payload.
598
599 These values will be passed to the callback function in this order, as
600 separate arguments."""
601 self._handler.requestRead([(0x13fc, "d")], self._handlePayloadCount,
602 extra = callback)
603
604 def requestTime(self, callback):
605 """Request the time from the simulator."""
606 self._handler.requestRead(Simulator.timeData, self._handleTime,
607 extra = callback)
608
609 def startMonitoring(self):
610 """Start the periodic monitoring of the aircraft and pass the resulting
611 state to the aircraft object periodically."""
612 assert not self._monitoringRequested
613 self._monitoringRequested = True
614
615 def stopMonitoring(self):
616 """Stop the periodic monitoring of the aircraft."""
617 assert self._monitoringRequested
618 self._monitoringRequested = False
619
620 def startFlare(self):
621 """Start monitoring the flare time.
622
623 At present it is assumed to be called from the FSUIPC thread, hence no
624 protection."""
625 #self._aircraft.logger.debug("startFlare")
626 if self._flareRequestID is None:
627 self._flareRates = []
628 self._flareRequestID = self._handler.requestPeriodicRead(0.1,
629 Simulator.flareData1,
630 self._handleFlare1)
631
632 def cancelFlare(self):
633 """Cancel monitoring the flare time.
634
635 At present it is assumed to be called from the FSUIPC thread, hence no
636 protection."""
637 if self._flareRequestID is not None:
638 self._handler.clearPeriodic(self._flareRequestID)
639 self._flareRequestID = None
640
641 def sendMessage(self, message, duration = 3,
642 _disconnect = False):
643 """Send a message to the pilot via the simulator.
644
645 duration is the number of seconds to keep the message displayed."""
646
647 if self._scroll:
648 if duration==0: duration = -1
649 elif duration == 1: duration = -2
650 else: duration = -duration
651
652 data = [(0x3380, -1 - len(message), message),
653 (0x32fa, 'h', duration)]
654
655 #if _disconnect:
656 # print "fsuipc.Simulator.sendMessage(disconnect)", message
657
658 self._handler.requestWrite(data, self._handleMessageSent,
659 extra = _disconnect)
660
661 def getFuel(self, callback):
662 """Get the fuel information for the current model.
663
664 The callback will be called with a list of triplets with the following
665 items:
666 - the fuel tank identifier
667 - the current weight of the fuel in the tank (in kgs)
668 - the current total capacity of the tank (in kgs)."""
669 if self._aircraftModel is None:
670 self._fuelCallback = callback
671 else:
672 self._aircraftModel.getFuel(self._handler, callback)
673
674 def setFuelLevel(self, levels):
675 """Set the fuel level to the given ones.
676
677 levels is an array of two-tuples, where each tuple consists of the
678 following:
679 - the const.FUELTANK_XXX constant denoting the tank that must be set,
680 - the requested level of the fuel as a floating-point value between 0.0
681 and 1.0."""
682 if self._aircraftModel is not None:
683 self._aircraftModel.setFuelLevel(self._handler, levels)
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 self._fsType = fsType
752 with self._hotkeyLock:
753 if self._hotkeys is not None:
754 self._hotkeySetGeneration += 1
755
756 self._handler.requestRead([(0x320c, "u")],
757 self._handleNumHotkeys,
758 (self._hotkeySetID,
759 self._hotkeySetGeneration))
760 self._connectionListener.connected(fsType, descriptor)
761
762 def connectionFailed(self):
763 """Called when the connection could not be established."""
764 with self._hotkeyLock:
765 self._clearHotkeyRequest()
766 self._connectionListener.connectionFailed()
767
768 def disconnected(self):
769 """Called when a connection to the flight simulator has been broken."""
770 with self._hotkeyLock:
771 self._clearHotkeyRequest()
772 self._connectionListener.disconnected()
773
774 def _startDefaultNormal(self):
775 """Start the default normal periodic request."""
776 assert self._normalRequestID is None
777 self._normalRequestID = \
778 self._handler.requestPeriodicRead(1.0,
779 Simulator.normalData,
780 self._handleNormal,
781 validator = self._validateNormal)
782
783 def _stopNormal(self):
784 """Stop the normal period request."""
785 assert self._normalRequestID is not None
786 self._handler.clearPeriodic(self._normalRequestID)
787 self._normalRequestID = None
788 self._monitoring = False
789
790 def _validateNormal(self, data, extra):
791 """Validate the normal data."""
792 return data[0]!=0 and data[1]!=0 and len(data[5])>0 and len(data[6])>0
793
794 def _handleNormal(self, data, extra):
795 """Handle the reply to the normal request.
796
797 At the beginning the result consists the data for normalData. When
798 monitoring is started, it contains the result also for the
799 aircraft-specific values.
800 """
801 timestamp = Simulator._getTimestamp(data)
802
803 createdNewModel = self._setAircraftName(timestamp, data[5], data[6])
804 if self._fuelCallback is not None:
805 self._aircraftModel.getFuel(self._handler, self._fuelCallback)
806 self._fuelCallback = None
807
808 self._scroll = data[7]!=0
809
810 if self._monitoringRequested and not self._monitoring:
811 self._stopNormal()
812 self._startMonitoring()
813 elif self._monitoring and not self._monitoringRequested:
814 self._stopNormal()
815 self._startDefaultNormal()
816 elif self._monitoring and self._aircraftModel is not None and \
817 not createdNewModel:
818 aircraftState = self._aircraftModel.getAircraftState(self._aircraft,
819 timestamp, data)
820
821 self._checkTimeSync(aircraftState)
822
823 self._aircraft.handleState(aircraftState)
824
825 def _checkTimeSync(self, aircraftState):
826 """Check if we need to synchronize the FS time."""
827 if not self._syncTime or aircraftState.paused or \
828 self._flareRequestID is not None:
829 self._nextSyncTime = -1
830 return
831
832 now = time.time()
833 seconds = time.gmtime(now).tm_sec
834
835 if seconds>30 and seconds<59:
836 if self._nextSyncTime > (now - 0.49):
837 return
838
839 self._handler.requestWrite([(0x023a, "b", int(seconds))],
840 self._handleTimeSynced)
841
842 #print "Set the seconds to ", seconds
843
844 if self._nextSyncTime<0:
845 self._nextSyncTime = now
846
847 self._nextSyncTime += Simulator.TIME_SYNC_INTERVAL
848 else:
849 self._nextSyncTime = -1
850
851 def _handleTimeSynced(self, success, extra):
852 """Callback for the time sync result."""
853 pass
854
855 def _setAircraftName(self, timestamp, name, airPath):
856 """Set the name of the aicraft and if it is different from the
857 previous, create a new model for it.
858
859 If so, also notifty the aircraft about the change.
860
861 Return if a new model was created."""
862 aircraftName = (name, airPath)
863 if aircraftName==self._aircraftName:
864 return False
865
866 self._aircraftName = aircraftName
867 needNew = self._aircraftModel is None
868 needNew = needNew or\
869 not self._aircraftModel.doesHandle(self._aircraft, aircraftName)
870 if not needNew:
871 specialModel = AircraftModel.findSpecial(self._aircraft, aircraftName)
872 needNew = specialModel is not None and \
873 specialModel is not self._aircraftModel.__class__
874
875 if needNew:
876 self._setAircraftModel(AircraftModel.create(self._aircraft, aircraftName))
877
878
879 self._aircraft.modelChanged(timestamp, self._latin1decoder(name)[0],
880 self._aircraftModel.name)
881
882 return needNew
883
884 def _setAircraftModel(self, model):
885 """Set a new aircraft model.
886
887 It will be queried for the data to monitor and the monitoring request
888 will be replaced by a new one."""
889 self._aircraftModel = model
890
891 if self._monitoring:
892 self._stopNormal()
893 self._startMonitoring()
894
895 def _startMonitoring(self):
896 """Start monitoring with the current aircraft model."""
897 data = Simulator.normalData[:]
898 self._aircraftModel.addMonitoringData(data, self._fsType)
899
900 self._normalRequestID = \
901 self._handler.requestPeriodicRead(1.0, data,
902 self._handleNormal,
903 validator = self._validateNormal)
904 self._monitoring = True
905
906 def _addFlareRate(self, data):
907 """Append a flare rate to the list of last rates."""
908 if len(self._flareRates)>=3:
909 del self._flareRates[0]
910 self._flareRates.append(Handler.fsuipc2VS(data))
911
912 def _handleFlare1(self, data, normal):
913 """Handle the first stage of flare monitoring."""
914 #self._aircraft.logger.debug("handleFlare1: " + str(data))
915 if Handler.fsuipc2radioAltitude(data[1])<=50.0:
916 self._flareStart = time.time()
917 self._flareStartFS = data[0]
918 self._handler.clearPeriodic(self._flareRequestID)
919 self._flareRequestID = \
920 self._handler.requestPeriodicRead(0.1,
921 Simulator.flareData2,
922 self._handleFlare2)
923 self._handler.requestRead(Simulator.flareStartData,
924 self._handleFlareStart)
925
926 self._addFlareRate(data[2])
927
928 def _handleFlareStart(self, data, extra):
929 """Handle the data need to notify the aircraft about the starting of
930 the flare."""
931 #self._aircraft.logger.debug("handleFlareStart: " + str(data))
932 if data is not None:
933 windDirection = data[1]*360.0/65536.0
934 if windDirection<0.0: windDirection += 360.0
935 self._aircraft.flareStarted(data[0], windDirection,
936 data[2]*1609.344/100.0,
937 self._flareStart, self._flareStartFS)
938
939 def _handleFlare2(self, data, normal):
940 """Handle the first stage of flare monitoring."""
941 #self._aircraft.logger.debug("handleFlare2: " + str(data))
942 if data[1]!=0:
943 flareEnd = time.time()
944 self._handler.clearPeriodic(self._flareRequestID)
945 self._flareRequestID = None
946
947 flareEndFS = data[0]
948 if flareEndFS<self._flareStartFS:
949 flareEndFS += 60
950
951 tdRate = Handler.fsuipc2VS(data[3])
952 tdRateCalculatedByFS = True
953 if tdRate==0 or tdRate>1000.0 or tdRate<-1000.0:
954 tdRate = min(self._flareRates)
955 tdRateCalculatedByFS = False
956
957 self._aircraft.flareFinished(flareEnd, flareEndFS,
958 tdRate, tdRateCalculatedByFS,
959 Handler.fsuipc2IAS(data[4]),
960 Handler.fsuipc2Degrees(data[5]),
961 Handler.fsuipc2Degrees(data[6]),
962 Handler.fsuipc2PositiveDegrees(data[7]))
963 else:
964 self._addFlareRate(data[2])
965
966 def _handleZFW(self, data, callback):
967 """Callback for a ZFW retrieval request."""
968 zfw = data[0] * const.LBSTOKG / 256.0
969 callback(zfw)
970
971 def _handleTime(self, data, callback):
972 """Callback for a time retrieval request."""
973 callback(Simulator._getTimestamp(data))
974
975 def _handlePayloadCount(self, data, callback):
976 """Callback for the payload count retrieval request."""
977 payloadCount = data[0]
978 data = [(0x3bfc, "d"), (0x30c0, "f")]
979 for i in range(0, payloadCount):
980 data.append((0x1400 + i*48, "f"))
981
982 self._handler.requestRead(data, self._handleWeights,
983 extra = callback)
984
985 def _handleWeights(self, data, callback):
986 """Callback for the weights retrieval request."""
987 zfw = data[0] * const.LBSTOKG / 256.0
988 grossWeight = data[1] * const.LBSTOKG
989 payload = sum(data[2:]) * const.LBSTOKG
990 dow = zfw - payload
991 callback(dow, payload, zfw, grossWeight)
992
993 def _handleMessageSent(self, success, disconnect):
994 """Callback for a message sending request."""
995 #print "fsuipc.Simulator._handleMessageSent", disconnect
996 if disconnect:
997 self._handler.disconnect()
998
999 def _handleNumHotkeys(self, data, (id, generation)):
1000 """Handle the result of the query of the number of hotkeys"""
1001 with self._hotkeyLock:
1002 if id==self._hotkeySetID and generation==self._hotkeySetGeneration:
1003 numHotkeys = data[0]
1004 print "fsuipc.Simulator._handleNumHotkeys: numHotkeys:", numHotkeys
1005 data = [(0x3210 + i*4, "d") for i in range(0, numHotkeys)]
1006 self._handler.requestRead(data, self._handleHotkeyTable,
1007 (id, generation))
1008
1009 def _setupHotkeys(self, data):
1010 """Setup the hiven hotkeys and return the data to be written.
1011
1012 If there were hotkeys set previously, they are reused as much as
1013 possible. Any of them not reused will be cleared."""
1014 hotkeys = self._hotkeys
1015 numHotkeys = len(hotkeys)
1016
1017 oldHotkeyOffsets = set([] if self._hotkeyOffets is None else
1018 self._hotkeyOffets)
1019
1020 self._hotkeyOffets = []
1021 numOffsets = 0
1022
1023 while oldHotkeyOffsets:
1024 offset = oldHotkeyOffsets.pop()
1025 self._hotkeyOffets.append(offset)
1026 numOffsets += 1
1027
1028 if numOffsets>=numHotkeys:
1029 break
1030
1031 for i in range(0, len(data)):
1032 if numOffsets>=numHotkeys:
1033 break
1034
1035 if data[i]==0:
1036 self._hotkeyOffets.append(0x3210 + i*4)
1037 numOffsets += 1
1038
1039 writeData = []
1040 for i in range(0, numOffsets):
1041 Simulator._appendHotkeyData(writeData,
1042 self._hotkeyOffets[i],
1043 hotkeys[i])
1044
1045 for offset in oldHotkeyOffsets:
1046 writeData.append((offset, "u", long(0)))
1047
1048 return writeData
1049
1050 def _handleHotkeyTable(self, data, (id, generation)):
1051 """Handle the result of the query of the hotkey table."""
1052 with self._hotkeyLock:
1053 if id==self._hotkeySetID and generation==self._hotkeySetGeneration:
1054 writeData = self._setupHotkeys(data)
1055 self._handler.requestWrite(writeData,
1056 self._handleHotkeysWritten,
1057 (id, generation))
1058
1059 def _handleHotkeysWritten(self, success, (id, generation)):
1060 """Handle the result of the hotkeys having been written."""
1061 with self._hotkeyLock:
1062 if success and id==self._hotkeySetID and \
1063 generation==self._hotkeySetGeneration:
1064 data = [(offset + 3, "b") for offset in self._hotkeyOffets]
1065
1066 self._hotkeyRequestID = \
1067 self._handler.requestPeriodicRead(0.5, data,
1068 self._handleHotkeys,
1069 (id, generation))
1070
1071 def _handleHotkeys(self, data, (id, generation)):
1072 """Handle the hotkeys."""
1073 with self._hotkeyLock:
1074 if id!=self._hotkeySetID or generation!=self._hotkeySetGeneration:
1075 return
1076
1077 callback = self._hotkeyCallback
1078 offsets = self._hotkeyOffets
1079
1080 hotkeysPressed = []
1081 for i in range(0, len(data)):
1082 if data[i]!=0:
1083 hotkeysPressed.append(i)
1084
1085 if hotkeysPressed:
1086 data = []
1087 for index in hotkeysPressed:
1088 data.append((offsets[index]+3, "b", int(0)))
1089 self._handler.requestWrite(data, self._handleHotkeysCleared)
1090
1091 callback(id, hotkeysPressed)
1092
1093 def _handleHotkeysCleared(self, sucess, extra):
1094 """Callback for the hotkey-clearing write request."""
1095
1096 def _clearHotkeyRequest(self):
1097 """Clear the hotkey request in the handler if there is any."""
1098 if self._hotkeyRequestID is not None:
1099 self._handler.clearPeriodic(self._hotkeyRequestID)
1100 self._hotkeyRequestID = None
1101
1102#------------------------------------------------------------------------------
1103
1104class AircraftModel(object):
1105 """Base class for the aircraft models.
1106
1107 Aircraft models handle the data arriving from FSUIPC and turn it into an
1108 object describing the aircraft's state."""
1109 monitoringData = [("paused", 0x0264, "H"),
1110 ("latitude", 0x0560, "l"),
1111 ("longitude", 0x0568, "l"),
1112 ("frozen", 0x3364, "H"),
1113 ("replay", 0x0628, "d"),
1114 ("slew", 0x05dc, "H"),
1115 ("overspeed", 0x036d, "b"),
1116 ("stalled", 0x036c, "b"),
1117 ("onTheGround", 0x0366, "H"),
1118 ("zfw", 0x3bfc, "d"),
1119 ("grossWeight", 0x30c0, "f"),
1120 ("heading", 0x0580, "d"),
1121 ("pitch", 0x0578, "d"),
1122 ("bank", 0x057c, "d"),
1123 ("ias", 0x02bc, "d"),
1124 ("mach", 0x11c6, "H"),
1125 ("groundSpeed", 0x02b4, "d"),
1126 ("vs", 0x02c8, "d"),
1127 ("radioAltitude", 0x31e4, "d"),
1128 ("altitude", 0x0570, "l"),
1129 ("gLoad", 0x11ba, "H"),
1130 ("flapsControl", 0x0bdc, "d"),
1131 ("flapsLeft", 0x0be0, "d"),
1132 ("flapsRight", 0x0be4, "d"),
1133 ("lights", 0x0d0c, "H"),
1134 ("pitot", 0x029c, "b"),
1135 ("parking", 0x0bc8, "H"),
1136 ("gearControl", 0x0be8, "d"),
1137 ("noseGear", 0x0bec, "d"),
1138 ("spoilersArmed", 0x0bcc, "d"),
1139 ("spoilers", 0x0bd0, "d"),
1140 ("altimeter", 0x0330, "H"),
1141 ("nav1", 0x0350, "H"),
1142 ("nav2", 0x0352, "H"),
1143 ("squawk", 0x0354, "H"),
1144 ("windSpeed", 0x0e90, "H"),
1145 ("windDirection", 0x0e92, "H"),
1146 ("visibility", 0x0e8a, "H"),
1147 ("cog", 0x2ef8, "f")]
1148
1149 specialModels = []
1150
1151 @staticmethod
1152 def registerSpecial(clazz):
1153 """Register the given class as a special model."""
1154 AircraftModel.specialModels.append(clazz)
1155
1156 @staticmethod
1157 def findSpecial(aircraft, aircraftName):
1158 for specialModel in AircraftModel.specialModels:
1159 if specialModel.doesHandle(aircraft, aircraftName):
1160 return specialModel
1161 return None
1162
1163 @staticmethod
1164 def create(aircraft, aircraftName):
1165 """Create the model for the given aircraft name, and notify the
1166 aircraft about it."""
1167 specialModel = AircraftModel.findSpecial(aircraft, aircraftName)
1168 if specialModel is not None:
1169 return specialModel()
1170 if aircraft.type in _genericModels:
1171 return _genericModels[aircraft.type]()
1172 else:
1173 return GenericModel()
1174
1175 @staticmethod
1176 def convertBCD(data, length):
1177 """Convert a data item encoded as BCD into a string of the given number
1178 of digits."""
1179 bcd = ""
1180 for i in range(0, length):
1181 digit = chr(ord('0') + (data&0x0f))
1182 data >>= 4
1183 bcd = digit + bcd
1184 return bcd
1185
1186 @staticmethod
1187 def convertFrequency(data):
1188 """Convert the given frequency data to a string."""
1189 bcd = AircraftModel.convertBCD(data, 4)
1190 return "1" + bcd[0:2] + "." + bcd[2:4]
1191
1192 def __init__(self, flapsNotches):
1193 """Construct the aircraft model.
1194
1195 flapsNotches is a list of degrees of flaps that are available on the aircraft."""
1196 self._flapsNotches = flapsNotches
1197
1198 @property
1199 def name(self):
1200 """Get the name for this aircraft model."""
1201 return "FSUIPC/Generic"
1202
1203 def doesHandle(self, aircraft, aircraftName):
1204 """Determine if the model handles the given aircraft name.
1205
1206 This default implementation returns False."""
1207 return False
1208
1209 def _addOffsetWithIndexMember(self, dest, offset, type, attrName = None):
1210 """Add the given FSUIPC offset and type to the given array and a member
1211 attribute with the given name."""
1212 dest.append((offset, type))
1213 if attrName is not None:
1214 setattr(self, attrName, len(dest)-1)
1215
1216 def _addDataWithIndexMembers(self, dest, prefix, data):
1217 """Add FSUIPC data to the given array and also corresponding index
1218 member variables with the given prefix.
1219
1220 data is a list of triplets of the following items:
1221 - the name of the data item. The index member variable will have a name
1222 created by prepending the given prefix to this name.
1223 - the FSUIPC offset
1224 - the FSUIPC type
1225
1226 The latter two items will be appended to dest."""
1227 for (name, offset, type) in data:
1228 self._addOffsetWithIndexMember(dest, offset, type, prefix + name)
1229
1230 def addMonitoringData(self, data, fsType):
1231 """Add the model-specific monitoring data to the given array."""
1232 self._addDataWithIndexMembers(data, "_monidx_",
1233 AircraftModel.monitoringData)
1234
1235 def getAircraftState(self, aircraft, timestamp, data):
1236 """Get an aircraft state object for the given monitoring data."""
1237 state = fs.AircraftState()
1238
1239 state.timestamp = timestamp
1240
1241 state.latitude = data[self._monidx_latitude] * \
1242 90.0 / 10001750.0 / 65536.0 / 65536.0
1243
1244 state.longitude = data[self._monidx_longitude] * \
1245 360.0 / 65536.0 / 65536.0 / 65536.0 / 65536.0
1246 if state.longitude>180.0: state.longitude = 360.0 - state.longitude
1247
1248 state.paused = data[self._monidx_paused]!=0 or \
1249 data[self._monidx_frozen]!=0 or \
1250 data[self._monidx_replay]!=0
1251 state.trickMode = data[self._monidx_slew]!=0
1252
1253 state.overspeed = data[self._monidx_overspeed]!=0
1254 state.stalled = data[self._monidx_stalled]!=0
1255 state.onTheGround = data[self._monidx_onTheGround]!=0
1256
1257 state.zfw = data[self._monidx_zfw] * const.LBSTOKG / 256.0
1258 state.grossWeight = data[self._monidx_grossWeight] * const.LBSTOKG
1259
1260 state.heading = Handler.fsuipc2PositiveDegrees(data[self._monidx_heading])
1261
1262 state.pitch = Handler.fsuipc2Degrees(data[self._monidx_pitch])
1263 state.bank = Handler.fsuipc2Degrees(data[self._monidx_bank])
1264
1265 state.ias = Handler.fsuipc2IAS(data[self._monidx_ias])
1266 state.mach = data[self._monidx_mach] / 20480.0
1267 state.groundSpeed = data[self._monidx_groundSpeed]* 3600.0/65536.0/1852.0
1268 state.vs = Handler.fsuipc2VS(data[self._monidx_vs])
1269
1270 state.radioAltitude = \
1271 Handler.fsuipc2radioAltitude(data[self._monidx_radioAltitude])
1272 state.altitude = data[self._monidx_altitude]/const.FEETTOMETRES/65536.0/65536.0
1273
1274 state.gLoad = data[self._monidx_gLoad] / 625.0
1275
1276 numNotchesM1 = len(self._flapsNotches) - 1
1277 flapsIncrement = 16383 / numNotchesM1
1278 flapsControl = data[self._monidx_flapsControl]
1279 flapsIndex = flapsControl / flapsIncrement
1280 if flapsIndex < numNotchesM1:
1281 if (flapsControl - (flapsIndex*flapsIncrement) >
1282 (flapsIndex+1)*flapsIncrement - flapsControl):
1283 flapsIndex += 1
1284 state.flapsSet = self._flapsNotches[flapsIndex]
1285
1286 flapsLeft = data[self._monidx_flapsLeft]
1287 state.flaps = self._flapsNotches[-1]*flapsLeft/16383.0
1288
1289 lights = data[self._monidx_lights]
1290
1291 state.navLightsOn = (lights&0x01) != 0
1292 state.antiCollisionLightsOn = (lights&0x02) != 0
1293 state.landingLightsOn = (lights&0x04) != 0
1294 state.strobeLightsOn = (lights&0x10) != 0
1295
1296 state.pitotHeatOn = data[self._monidx_pitot]!=0
1297
1298 state.parking = data[self._monidx_parking]!=0
1299
1300 state.gearControlDown = data[self._monidx_gearControl]==16383
1301 state.gearsDown = data[self._monidx_noseGear]==16383
1302
1303 state.spoilersArmed = data[self._monidx_spoilersArmed]!=0
1304
1305 spoilers = data[self._monidx_spoilers]
1306 if spoilers<=4800:
1307 state.spoilersExtension = 0.0
1308 else:
1309 state.spoilersExtension = (spoilers - 4800) * 100.0 / (16383 - 4800)
1310
1311 state.altimeter = data[self._monidx_altimeter] / 16.0
1312
1313 state.nav1 = AircraftModel.convertFrequency(data[self._monidx_nav1])
1314 state.nav2 = AircraftModel.convertFrequency(data[self._monidx_nav2])
1315 state.squawk = AircraftModel.convertBCD(data[self._monidx_squawk], 4)
1316
1317 state.windSpeed = data[self._monidx_windSpeed]
1318 state.windDirection = data[self._monidx_windDirection]*360.0/65536.0
1319 if state.windDirection<0.0: state.windDirection += 360.0
1320
1321 state.visibility = data[self._monidx_visibility]*1609.344/100.0
1322
1323 state.cog = data[self._monidx_cog]
1324
1325 return state
1326
1327#------------------------------------------------------------------------------
1328
1329class GenericAircraftModel(AircraftModel):
1330 """A generic aircraft model that can handle the fuel levels, the N1 or RPM
1331 values and some other common parameters in a generic way."""
1332
1333 def __init__(self, flapsNotches, fuelTanks, numEngines, isN1 = True):
1334 """Construct the generic aircraft model with the given data.
1335
1336 flapsNotches is an array of how much degrees the individual flaps
1337 notches mean.
1338
1339 fuelTanks is an array of const.FUELTANK_XXX constants about the
1340 aircraft's fuel tanks. They will be converted to offsets.
1341
1342 numEngines is the number of engines the aircraft has.
1343
1344 isN1 determines if the engines have an N1 value or an RPM value
1345 (e.g. pistons)."""
1346 super(GenericAircraftModel, self).__init__(flapsNotches = flapsNotches)
1347
1348 self._fuelTanks = fuelTanks
1349 self._fuelStartIndex = None
1350 self._numEngines = numEngines
1351 self._engineStartIndex = None
1352 self._isN1 = isN1
1353
1354 def doesHandle(self, aircraft, aircraftName):
1355 """Determine if the model handles the given aircraft name.
1356
1357 This implementation returns True."""
1358 return True
1359
1360 def addMonitoringData(self, data, fsType):
1361 """Add the model-specific monitoring data to the given array."""
1362 super(GenericAircraftModel, self).addMonitoringData(data, fsType)
1363
1364 self._fuelStartIndex = self._addFuelOffsets(data, "_monidx_fuelWeight")
1365
1366 self._engineStartIndex = len(data)
1367 for i in range(0, self._numEngines):
1368 self._addOffsetWithIndexMember(data, 0x088c + i * 0x98, "h") # throttle lever
1369 if self._isN1:
1370 self._addOffsetWithIndexMember(data, 0x2000 + i * 0x100, "f") # N1
1371 else:
1372 self._addOffsetWithIndexMember(data, 0x0898 + i * 0x98, "H") # RPM
1373 self._addOffsetWithIndexMember(data, 0x08c8 + i * 0x98, "H") # RPM scaler
1374
1375 def getAircraftState(self, aircraft, timestamp, data):
1376 """Get the aircraft state.
1377
1378 Get it from the parent, and then add the data about the fuel levels and
1379 the engine parameters."""
1380 state = super(GenericAircraftModel, self).getAircraftState(aircraft,
1381 timestamp,
1382 data)
1383
1384 (state.fuel, state.totalFuel) = \
1385 self._convertFuelData(data, index = self._monidx_fuelWeight)
1386
1387 state.n1 = [] if self._isN1 else None
1388 state.rpm = None if self._isN1 else []
1389 itemsPerEngine = 2 if self._isN1 else 3
1390
1391 state.reverser = []
1392 for i in range(self._engineStartIndex,
1393 self._engineStartIndex +
1394 itemsPerEngine*self._numEngines,
1395 itemsPerEngine):
1396 state.reverser.append(data[i]<0)
1397 if self._isN1:
1398 state.n1.append(data[i+1])
1399 else:
1400 state.rpm.append(data[i+1] * data[i+2]/65536.0)
1401
1402 return state
1403
1404 def getFuel(self, handler, callback):
1405 """Get the fuel information for this model.
1406
1407 See Simulator.getFuel for more information. This
1408 implementation simply queries the fuel tanks given to the
1409 constructor."""
1410 data = []
1411 self._addFuelOffsets(data)
1412
1413 handler.requestRead(data, self._handleFuelRetrieved,
1414 extra = callback)
1415
1416 def setFuelLevel(self, handler, levels):
1417 """Set the fuel level.
1418
1419 See the description of Simulator.setFuelLevel. This
1420 implementation simply sets the fuel tanks as given."""
1421 data = []
1422 for (tank, level) in levels:
1423 offset = _tank2offset[tank]
1424 value = long(level * 128.0 * 65536.0)
1425 data.append( (offset, "u", value) )
1426
1427 handler.requestWrite(data, self._handleFuelWritten)
1428
1429 def _addFuelOffsets(self, data, weightIndexName = None):
1430 """Add the fuel offsets to the given data array.
1431
1432 If weightIndexName is not None, it will be the name of the
1433 fuel weight index.
1434
1435 Returns the index of the first fuel tank's data."""
1436 self._addOffsetWithIndexMember(data, 0x0af4, "H", weightIndexName)
1437
1438 fuelStartIndex = len(data)
1439 for tank in self._fuelTanks:
1440 offset = _tank2offset[tank]
1441 self._addOffsetWithIndexMember(data, offset, "u") # tank level
1442 self._addOffsetWithIndexMember(data, offset+4, "u") # tank capacity
1443
1444 return fuelStartIndex
1445
1446 def _convertFuelData(self, data, index = 0, addCapacities = False):
1447 """Convert the given data into a fuel info list.
1448
1449 The list consists of two or three-tuples of the following
1450 items:
1451 - the fuel tank ID,
1452 - the amount of the fuel in kg,
1453 - if addCapacities is True, the total capacity of the tank."""
1454 fuelWeight = data[index] / 256.0
1455 index += 1
1456
1457 result = []
1458 totalFuel = 0
1459 for fuelTank in self._fuelTanks:
1460 capacity = data[index+1] * fuelWeight * const.LBSTOKG
1461 amount = data[index] * capacity / 128.0 / 65536.0
1462 index += 2
1463
1464 result.append( (fuelTank, amount, capacity) if addCapacities
1465 else (fuelTank, amount))
1466 totalFuel += amount
1467
1468 return (result, totalFuel)
1469
1470 def _handleFuelRetrieved(self, data, callback):
1471 """Callback for a fuel retrieval request."""
1472 (fuelData, _totalFuel) = self._convertFuelData(data,
1473 addCapacities = True)
1474 callback(fuelData)
1475
1476 def _handleFuelWritten(self, success, extra):
1477 """Callback for a fuel setting request."""
1478 pass
1479
1480#------------------------------------------------------------------------------
1481
1482class GenericModel(GenericAircraftModel):
1483 """Generic aircraft model for an unknown type."""
1484 def __init__(self):
1485 """Construct the model."""
1486 super(GenericModel, self). \
1487 __init__(flapsNotches = [0, 10, 20, 30],
1488 fuelTanks = [const.FUELTANK_LEFT, const.FUELTANK_RIGHT],
1489 numEngines = 2)
1490
1491 @property
1492 def name(self):
1493 """Get the name for this aircraft model."""
1494 return "FSUIPC/Generic"
1495
1496#------------------------------------------------------------------------------
1497
1498class B737Model(GenericAircraftModel):
1499 """Generic model for the Boeing 737 Classing and NG aircraft."""
1500 fuelTanks = [const.FUELTANK_LEFT, const.FUELTANK_CENTRE, const.FUELTANK_RIGHT]
1501
1502 def __init__(self):
1503 """Construct the model."""
1504 super(B737Model, self). \
1505 __init__(flapsNotches = [0, 1, 2, 5, 10, 15, 25, 30, 40],
1506 fuelTanks = B737Model.fuelTanks,
1507 numEngines = 2)
1508
1509 @property
1510 def name(self):
1511 """Get the name for this aircraft model."""
1512 return "FSUIPC/Generic Boeing 737"
1513
1514#------------------------------------------------------------------------------
1515
1516class PMDGBoeing737NGModel(B737Model):
1517 """A model handler for the PMDG Boeing 737NG model."""
1518 @staticmethod
1519 def doesHandle(aircraft, (name, airPath)):
1520 """Determine if this model handler handles the aircraft with the given
1521 name."""
1522 return aircraft.type in [const.AIRCRAFT_B736,
1523 const.AIRCRAFT_B737,
1524 const.AIRCRAFT_B738,
1525 const.AIRCRAFT_B738C] and \
1526 (name.find("PMDG")!=-1 or airPath.find("PMDG")!=-1) and \
1527 (name.find("737")!=-1 or airPath.find("737")!=-1) and \
1528 (name.find("600")!=-1 or airPath.find("600")!=-1 or \
1529 name.find("700")!=-1 or airPath.find("700")!=-1 or \
1530 name.find("800")!=-1 or airPath.find("800")!=-1 or \
1531 name.find("900")!=-1 or airPath.find("900")!=-1)
1532
1533 @property
1534 def name(self):
1535 """Get the name for this aircraft model."""
1536 return "FSUIPC/PMDG Boeing 737NG(X)"
1537
1538 def addMonitoringData(self, data, fsType):
1539 """Add the model-specific monitoring data to the given array."""
1540 self._fsType = fsType
1541
1542 super(PMDGBoeing737NGModel, self).addMonitoringData(data, fsType)
1543
1544 self._addOffsetWithIndexMember(data, 0x6202, "b", "_pmdgidx_switches")
1545
1546 if fsType==const.SIM_MSFSX:
1547 print "FSX detected, adding position lights switch offset"
1548 self._addOffsetWithIndexMember(data, 0x6500, "b",
1549 "_pmdgidx_lts_positionsw")
1550
1551 def getAircraftState(self, aircraft, timestamp, data):
1552 """Get the aircraft state.
1553
1554 Get it from the parent, and then check some PMDG-specific stuff."""
1555 state = super(PMDGBoeing737NGModel, self).getAircraftState(aircraft,
1556 timestamp,
1557 data)
1558 if data[self._pmdgidx_switches]&0x01==0x01:
1559 state.altimeter = 1013.25
1560
1561 if self._fsType==const.SIM_MSFSX:
1562 state.strobeLightsOn = data[self._pmdgidx_lts_positionsw]==0x02
1563
1564 return state
1565
1566#------------------------------------------------------------------------------
1567
1568class B767Model(GenericAircraftModel):
1569 """Generic model for the Boeing 767 aircraft."""
1570 fuelTanks = [const.FUELTANK_LEFT, const.FUELTANK_CENTRE, const.FUELTANK_RIGHT]
1571
1572 def __init__(self):
1573 """Construct the model."""
1574 super(B767Model, self). \
1575 __init__(flapsNotches = [0, 1, 5, 15, 20, 25, 30],
1576 fuelTanks = Boeing767Model.fuelTanks,
1577 numEngines = 2)
1578
1579 @property
1580 def name(self):
1581 """Get the name for this aircraft model."""
1582 return "FSUIPC/Generic Boeing 767"
1583
1584#------------------------------------------------------------------------------
1585
1586class DH8DModel(GenericAircraftModel):
1587 """Generic model for the Bombardier Dash 8-Q400 aircraft."""
1588 fuelTanks = [const.FUELTANK_LEFT, const.FUELTANK_RIGHT]
1589
1590 def __init__(self):
1591 """Construct the model."""
1592 super(DH8DModel, self). \
1593 __init__(flapsNotches = [0, 5, 10, 15, 35],
1594 fuelTanks = DH8DModel.fuelTanks,
1595 numEngines = 2)
1596
1597 @property
1598 def name(self):
1599 """Get the name for this aircraft model."""
1600 return "FSUIPC/Generic Bombardier Dash 8-Q400"
1601
1602#------------------------------------------------------------------------------
1603
1604class DreamwingsDH8DModel(DH8DModel):
1605 """Model handler for the Dreamwings Dash 8-Q400."""
1606 @staticmethod
1607 def doesHandle(aircraft, (name, airPath)):
1608 """Determine if this model handler handles the aircraft with the given
1609 name."""
1610 return aircraft.type==const.AIRCRAFT_DH8D and \
1611 (name.find("Dreamwings")!=-1 or airPath.find("Dreamwings")!=-1) and \
1612 (name.find("Dash")!=-1 or airPath.find("Dash")!=-1) and \
1613 (name.find("Q400")!=-1 or airPath.find("Q400")!=-1) and \
1614 airPath.find("Dash8Q400")!=-1
1615
1616 @property
1617 def name(self):
1618 """Get the name for this aircraft model."""
1619 return "FSUIPC/Dreamwings Bombardier Dash 8-Q400"
1620
1621 def getAircraftState(self, aircraft, timestamp, data):
1622 """Get the aircraft state.
1623
1624 Get it from the parent, and then invert the pitot heat state."""
1625 state = super(DreamwingsDH8DModel, self).getAircraftState(aircraft,
1626 timestamp,
1627 data)
1628 state.pitotHeatOn = not state.pitotHeatOn
1629
1630 return state
1631#------------------------------------------------------------------------------
1632
1633class CRJ2Model(GenericAircraftModel):
1634 """Generic model for the Bombardier CRJ-200 aircraft."""
1635 fuelTanks = [const.FUELTANK_LEFT, const.FUELTANK_CENTRE, const.FUELTANK_RIGHT]
1636
1637 def __init__(self):
1638 """Construct the model."""
1639 super(CRJ2Model, self). \
1640 __init__(flapsNotches = [0, 8, 20, 30, 45],
1641 fuelTanks = CRJ2Model.fuelTanks,
1642 numEngines = 2)
1643
1644 @property
1645 def name(self):
1646 """Get the name for this aircraft model."""
1647 return "FSUIPC/Generic Bombardier CRJ-200"
1648
1649#------------------------------------------------------------------------------
1650
1651class F70Model(GenericAircraftModel):
1652 """Generic model for the Fokker F70 aircraft."""
1653 fuelTanks = [const.FUELTANK_LEFT, const.FUELTANK_CENTRE, const.FUELTANK_RIGHT]
1654
1655 def __init__(self):
1656 """Construct the model."""
1657 super(F70Model, self). \
1658 __init__(flapsNotches = [0, 8, 15, 25, 42],
1659 fuelTanks = F70Model.fuelTanks,
1660 numEngines = 2)
1661
1662 @property
1663 def name(self):
1664 """Get the name for this aircraft model."""
1665 return "FSUIPC/Generic Fokker 70"
1666
1667#------------------------------------------------------------------------------
1668
1669class DC3Model(GenericAircraftModel):
1670 """Generic model for the Lisunov Li-2 (DC-3) aircraft."""
1671 fuelTanks = [const.FUELTANK_LEFT, const.FUELTANK_CENTRE,
1672 const.FUELTANK_RIGHT]
1673 # fuelTanks = [const.FUELTANK_LEFT_AUX, const.FUELTANK_LEFT,
1674 # const.FUELTANK_RIGHT, const.FUELTANK_RIGHT_AUX]
1675
1676 def __init__(self):
1677 """Construct the model."""
1678 super(DC3Model, self). \
1679 __init__(flapsNotches = [0, 15, 30, 45],
1680 fuelTanks = DC3Model.fuelTanks,
1681 numEngines = 2, isN1 = False)
1682 self._leftLevel = 0.0
1683 self._rightLevel = 0.0
1684
1685 @property
1686 def name(self):
1687 """Get the name for this aircraft model."""
1688 return "FSUIPC/Generic Lisunov Li-2 (DC-3)"
1689
1690 def _convertFuelData(self, data, index = 0, addCapacities = False):
1691 """Convert the given data into a fuel info list.
1692
1693 It assumes to receive the 3 fuel tanks as seen above (left,
1694 centre and right) and converts it to left aux, left, right,
1695 and right aux. The amount in the left tank goes into left aux,
1696 the amount of the right tank goes into right aux and the
1697 amount of the centre tank goes into the left and right tanks
1698 evenly distributed."""
1699 (rawFuelData, totalFuel) = \
1700 super(DC3Model, self)._convertFuelData(data, index, addCapacities)
1701
1702 centreAmount = rawFuelData[1][1]
1703 if addCapacities:
1704 centreCapacity = rawFuelData[1][2]
1705 self._leftLevel = self._rightLevel = \
1706 centreAmount / centreCapacity / 2.0
1707 fuelData = [(const.FUELTANK_LEFT_AUX,
1708 rawFuelData[0][1], rawFuelData[0][2]),
1709 (const.FUELTANK_LEFT,
1710 centreAmount/2.0, centreCapacity/2.0),
1711 (const.FUELTANK_RIGHT,
1712 centreAmount/2.0, centreCapacity/2.0),
1713 (const.FUELTANK_RIGHT_AUX,
1714 rawFuelData[2][1], rawFuelData[2][2])]
1715 else:
1716 fuelData = [(const.FUELTANK_LEFT_AUX, rawFuelData[0][1]),
1717 (const.FUELTANK_LEFT, centreAmount/2.0),
1718 (const.FUELTANK_RIGHT, centreAmount/2.0),
1719 (const.FUELTANK_RIGHT_AUX, rawFuelData[2][1])]
1720
1721 return (fuelData, totalFuel)
1722
1723 def setFuelLevel(self, handler, levels):
1724 """Set the fuel level.
1725
1726 See the description of Simulator.setFuelLevel. This
1727 implementation assumes to get the four-tank representation,
1728 as returned by getFuel()."""
1729 leftLevel = None
1730 centreLevel = None
1731 rightLevel = None
1732
1733 for (tank, level) in levels:
1734 if tank==const.FUELTANK_LEFT_AUX:
1735 leftLevel = level if leftLevel is None else (leftLevel + level)
1736 elif tank==const.FUELTANK_LEFT:
1737 level /= 2.0
1738 centreLevel = (self._rightLevel + level) \
1739 if centreLevel is None else (centreLevel + level)
1740 self._leftLevel = level
1741 elif tank==const.FUELTANK_RIGHT:
1742 level /= 2.0
1743 centreLevel = (self._leftLevel + level) \
1744 if centreLevel is None else (centreLevel + level)
1745 self._rightLevel = level
1746 elif tank==const.FUELTANK_RIGHT_AUX:
1747 rightLevel = level if rightLevel is None \
1748 else (rightLevel + level)
1749
1750 levels = []
1751 if leftLevel is not None: levels.append((const.FUELTANK_LEFT,
1752 leftLevel))
1753 if centreLevel is not None: levels.append((const.FUELTANK_CENTRE,
1754 centreLevel))
1755 if rightLevel is not None: levels.append((const.FUELTANK_RIGHT,
1756 rightLevel))
1757
1758 super(DC3Model, self).setFuelLevel(handler, levels)
1759
1760#------------------------------------------------------------------------------
1761
1762class T134Model(GenericAircraftModel):
1763 """Generic model for the Tupolev Tu-134 aircraft."""
1764 fuelTanks = [const.FUELTANK_LEFT_TIP, const.FUELTANK_EXTERNAL1,
1765 const.FUELTANK_LEFT_AUX,
1766 const.FUELTANK_CENTRE,
1767 const.FUELTANK_RIGHT_AUX,
1768 const.FUELTANK_EXTERNAL2, const.FUELTANK_RIGHT_TIP]
1769
1770 def __init__(self):
1771 """Construct the model."""
1772 super(T134Model, self). \
1773 __init__(flapsNotches = [0, 10, 20, 30],
1774 fuelTanks = T134Model.fuelTanks,
1775 numEngines = 2)
1776
1777 @property
1778 def name(self):
1779 """Get the name for this aircraft model."""
1780 return "FSUIPC/Generic Tupolev Tu-134"
1781
1782#------------------------------------------------------------------------------
1783
1784class T154Model(GenericAircraftModel):
1785 """Generic model for the Tupolev Tu-134 aircraft."""
1786 fuelTanks = [const.FUELTANK_LEFT_AUX, const.FUELTANK_LEFT,
1787 const.FUELTANK_CENTRE, const.FUELTANK_CENTRE2,
1788 const.FUELTANK_RIGHT, const.FUELTANK_RIGHT_AUX]
1789
1790 def __init__(self):
1791 """Construct the model."""
1792 super(T154Model, self). \
1793 __init__(flapsNotches = [0, 15, 28, 45],
1794 fuelTanks = T154Model.fuelTanks,
1795 numEngines = 3)
1796
1797 @property
1798 def name(self):
1799 """Get the name for this aircraft model."""
1800 return "FSUIPC/Generic Tupolev Tu-154"
1801
1802 def getAircraftState(self, aircraft, timestamp, data):
1803 """Get an aircraft state object for the given monitoring data.
1804
1805 This removes the reverser value for the middle engine."""
1806 state = super(T154Model, self).getAircraftState(aircraft, timestamp, data)
1807 del state.reverser[1]
1808 return state
1809
1810#------------------------------------------------------------------------------
1811
1812class YK40Model(GenericAircraftModel):
1813 """Generic model for the Yakovlev Yak-40 aircraft."""
1814 fuelTanks = [const.FUELTANK_LEFT, const.FUELTANK_RIGHT]
1815
1816 def __init__(self):
1817 """Construct the model."""
1818 super(YK40Model, self). \
1819 __init__(flapsNotches = [0, 20, 35],
1820 fuelTanks = YK40Model.fuelTanks,
1821 numEngines = 2)
1822
1823 @property
1824 def name(self):
1825 """Get the name for this aircraft model."""
1826 return "FSUIPC/Generic Yakovlev Yak-40"
1827
1828#------------------------------------------------------------------------------
1829
1830_genericModels = { const.AIRCRAFT_B736 : B737Model,
1831 const.AIRCRAFT_B737 : B737Model,
1832 const.AIRCRAFT_B738 : B737Model,
1833 const.AIRCRAFT_B738C : B737Model,
1834 const.AIRCRAFT_B733 : B737Model,
1835 const.AIRCRAFT_B734 : B737Model,
1836 const.AIRCRAFT_B735 : B737Model,
1837 const.AIRCRAFT_DH8D : DH8DModel,
1838 const.AIRCRAFT_B762 : B767Model,
1839 const.AIRCRAFT_B763 : B767Model,
1840 const.AIRCRAFT_CRJ2 : B767Model,
1841 const.AIRCRAFT_F70 : F70Model,
1842 const.AIRCRAFT_DC3 : DC3Model,
1843 const.AIRCRAFT_T134 : T134Model,
1844 const.AIRCRAFT_T154 : T154Model,
1845 const.AIRCRAFT_YK40 : YK40Model }
1846
1847#------------------------------------------------------------------------------
1848
1849AircraftModel.registerSpecial(PMDGBoeing737NGModel)
1850AircraftModel.registerSpecial(DreamwingsDH8DModel)
1851
1852#------------------------------------------------------------------------------
Note: See TracBrowser for help on using the repository browser.