source: src/mlx/fsuipc.py@ 285:18596b688f06

Last change on this file since 285:18596b688f06 was 274:b866c39279e8, checked in by István Váradi <ivaradi@…>, 12 years ago

Reworked fuel handling so that the model can tell what tanks there are

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