source: src/mlx/fsuipc.py@ 314:09f49b9eef64

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

Added support for the landing lights and the NAV frequencies being unreliable and the Digital Aviation F70 model

File size: 74.1 KB
Line 
1
2import fs
3import const
4import util
5import acft
6
7import threading
8import os
9import time
10import calendar
11import sys
12import codecs
13
14if os.name == "nt" and "FORCE_PYUIPC_SIM" not in os.environ:
15 import pyuipc
16else:
17 import pyuipc_sim as pyuipc
18
19#------------------------------------------------------------------------------
20
21## @package mlx.fsuipc
22#
23# The module towards FSUIPC.
24#
25# This module implements the simulator interface to FSUIPC.
26#
27# The \ref Handler class is thread handling the FSUIPC requests. It can be
28# given read, periodic read and write, requests, that are executed
29# asynchronously, and a callback function is called with the result. This class
30# is used internally within the module.
31#
32# The \ref Simulator class is the actual interface to the flight simulator, and
33# an instance of it is returned by \ref mlx.fs.createSimulator. This object can
34# be used to connect to the simulator and disconnect from it, to query various
35# data and to start and stop the monitoring of the data.
36#
37# \ref AircraftModel is the base class of the aircraft models. A "model" is a
38# particular implementation of an aircraft, such as the PMDG Boeing 737NG in
39# Flight Simulator 2004. Since each type and each model has its peculiarities
40# (e.g. the different engine and fuel tank configurations), each aircraft type
41# has a generic model, that should work for most of the time. However, certain
42# models may implement various controls, gauges, etc. differently, and such
43# peculiarites can be handled in a specific subclass of \ref
44# AircraftModel. These subclasses should be registered as special ones, and if
45# the simulator detects that the aircraft's model became known or has changed,
46# it will check these special models before resorting to the generic ones.
47#
48# The models are responsible also for querying certain parameters, such as the
49# fuel tank configuration. While ideally it should be specific to a type only,
50# it is possible that the model contains different tanks, in which case some
51# tricks may be needed. See the \ref DC3Model "DC-3 (Li-2)" aircraft as an
52# example.
53
54#------------------------------------------------------------------------------
55
56## The mapping of tank types to FSUIPC offsets
57_tank2offset = { const.FUELTANK_CENTRE : 0x0b74,
58 const.FUELTANK_LEFT : 0x0b7c,
59 const.FUELTANK_RIGHT : 0x0b94,
60 const.FUELTANK_LEFT_AUX : 0x0b84,
61 const.FUELTANK_RIGHT_AUX : 0x0b9c,
62 const.FUELTANK_LEFT_TIP : 0x0b8c,
63 const.FUELTANK_RIGHT_TIP : 0x0ba4,
64 const.FUELTANK_EXTERNAL1 : 0x1254,
65 const.FUELTANK_EXTERNAL2 : 0x125c,
66 const.FUELTANK_CENTRE2 : 0x1244 }
67
68#------------------------------------------------------------------------------
69
70class Handler(threading.Thread):
71 """The thread to handle the FSUIPC requests."""
72 @staticmethod
73 def fsuipc2VS(data):
74 """Convert the given vertical speed data read from FSUIPC into feet/min."""
75 return data*60.0/const.FEETTOMETRES/256.0
76
77 @staticmethod
78 def fsuipc2radioAltitude(data):
79 """Convert the given radio altitude data read from FSUIPC into feet."""
80 return data/const.FEETTOMETRES/65536.0
81
82 @staticmethod
83 def fsuipc2Degrees(data):
84 """Convert the given data into degrees."""
85 return data * 360.0 / 65536.0 / 65536.0
86
87 @staticmethod
88 def fsuipc2PositiveDegrees(data):
89 """Convert the given data into positive degrees."""
90 degrees = Handler.fsuipc2Degrees(data)
91 if degrees<0.0: degrees += 360.0
92 return degrees
93
94 @staticmethod
95 def fsuipc2IAS(data):
96 """Convert the given data into indicated airspeed."""
97 return data / 128.0
98
99 @staticmethod
100 def _callSafe(fun):
101 """Call the given function and swallow any exceptions."""
102 try:
103 return fun()
104 except Exception, e:
105 print >> sys.stderr, str(e)
106 return None
107
108 # The number of times a read is attempted
109 NUM_READATTEMPTS = 3
110
111 # The number of connection attempts
112 NUM_CONNECTATTEMPTS = 3
113
114 # The interval between successive connect attempts
115 CONNECT_INTERVAL = 0.25
116
117 @staticmethod
118 def _performRead(data, callback, extra, validator):
119 """Perform a read request.
120
121 If there is a validator, that will be called with the return values,
122 and if the values are wrong, the request is retried at most a certain
123 number of times.
124
125 Return True if the request has succeeded, False if validation has
126 failed during all attempts. An exception may also be thrown if there is
127 some lower-level communication problem."""
128 attemptsLeft = Handler.NUM_READATTEMPTS
129 while attemptsLeft>0:
130 values = pyuipc.read(data)
131 if validator is None or \
132 Handler._callSafe(lambda: validator(values, extra)):
133 Handler._callSafe(lambda: callback(values, extra))
134 return True
135 else:
136 attemptsLeft -= 1
137 return False
138
139 class Request(object):
140 """A simple, one-shot request."""
141 def __init__(self, forWrite, data, callback, extra, validator = None):
142 """Construct the request."""
143 self._forWrite = forWrite
144 self._data = data
145 self._callback = callback
146 self._extra = extra
147 self._validator = validator
148
149 def process(self, time):
150 """Process the request.
151
152 Return True if the request has succeeded, False if data validation
153 has failed for a reading request. An exception may also be thrown
154 if there is some lower-level communication problem."""
155 if self._forWrite:
156 pyuipc.write(self._data)
157 Handler._callSafe(lambda: self._callback(True, self._extra))
158 return True
159 else:
160 return Handler._performRead(self._data, self._callback,
161 self._extra, self._validator)
162
163 def fail(self):
164 """Handle the failure of this request."""
165 if self._forWrite:
166 Handler._callSafe(lambda: self._callback(False, self._extra))
167 else:
168 Handler._callSafe(lambda: self._callback(None, self._extra))
169
170 class PeriodicRequest(object):
171 """A periodic request."""
172 def __init__(self, id, period, data, callback, extra, validator):
173 """Construct the periodic request."""
174 self._id = id
175 self._period = period
176 self._nextFire = time.time()
177 self._data = data
178 self._preparedData = None
179 self._callback = callback
180 self._extra = extra
181 self._validator = validator
182
183 @property
184 def id(self):
185 """Get the ID of this periodic request."""
186 return self._id
187
188 @property
189 def nextFire(self):
190 """Get the next firing time."""
191 return self._nextFire
192
193 def process(self, time):
194 """Check if this request should be executed, and if so, do so.
195
196 time is the time at which the request is being executed. If this
197 function is called too early, nothing is done, and True is
198 returned.
199
200 Return True if the request has succeeded, False if data validation
201 has failed. An exception may also be thrown if there is some
202 lower-level communication problem."""
203 if time<self._nextFire:
204 return True
205
206 if self._preparedData is None:
207 self._preparedData = pyuipc.prepare_data(self._data)
208 self._data = None
209
210 isOK = Handler._performRead(self._preparedData, self._callback,
211 self._extra, self._validator)
212
213 if isOK:
214 while self._nextFire <= time:
215 self._nextFire += self._period
216
217 return isOK
218
219 def fail(self):
220 """Handle the failure of this request."""
221 pass
222
223 def __cmp__(self, other):
224 """Compare two periodic requests. They are ordered by their next
225 firing times."""
226 return cmp(self._nextFire, other._nextFire)
227
228 def __init__(self, connectionListener,
229 connectAttempts = -1, connectInterval = 0.2):
230 """Construct the handler with the given connection listener."""
231 threading.Thread.__init__(self)
232
233 self._connectionListener = connectionListener
234 self._connectAttempts = connectAttempts
235 self._connectInterval = connectInterval
236
237 self._requestCondition = threading.Condition()
238 self._connectionRequested = False
239 self._connected = False
240
241 self._requests = []
242 self._nextPeriodicID = 1
243 self._periodicRequests = []
244
245 self.daemon = True
246
247 def requestRead(self, data, callback, extra = None, validator = None):
248 """Request the reading of some data.
249
250 data is a list of tuples of the following items:
251 - the offset of the data as an integer
252 - the type letter of the data as a string
253
254 callback is a function that receives two pieces of data:
255 - the values retrieved or None on error
256 - the extra parameter
257
258 It will be called in the handler's thread!
259 """
260 with self._requestCondition:
261 self._requests.append(Handler.Request(False, data, callback, extra,
262 validator))
263 self._requestCondition.notify()
264
265 def requestWrite(self, data, callback, extra = None):
266 """Request the writing of some data.
267
268 data is a list of tuples of the following items:
269 - the offset of the data as an integer
270 - the type letter of the data as a string
271 - the data to write
272
273 callback is a function that receives two pieces of data:
274 - a boolean indicating if writing was successful
275 - the extra data
276 It will be called in the handler's thread!
277 """
278 with self._requestCondition:
279 request = Handler.Request(True, data, callback, extra)
280 #print "fsuipc.Handler.requestWrite", request
281 self._requests.append(request)
282 self._requestCondition.notify()
283
284 @staticmethod
285 def _readWriteCallback(data, extra):
286 """Callback for the read() and write() calls below."""
287 extra.append(data)
288 with extra[0] as condition:
289 condition.notify()
290
291 def requestPeriodicRead(self, period, data, callback, extra = None,
292 validator = None):
293 """Request a periodic read of data.
294
295 period is a floating point number with the period in seconds.
296
297 This function returns an identifier which can be used to cancel the
298 request."""
299 with self._requestCondition:
300 id = self._nextPeriodicID
301 self._nextPeriodicID += 1
302 request = Handler.PeriodicRequest(id, period, data, callback,
303 extra, validator)
304 self._periodicRequests.append(request)
305 self._requestCondition.notify()
306 return id
307
308 def clearPeriodic(self, id):
309 """Clear the periodic request with the given ID."""
310 with self._requestCondition:
311 for i in range(0, len(self._periodicRequests)):
312 if self._periodicRequests[i].id==id:
313 del self._periodicRequests[i]
314 return True
315 return False
316
317 def connect(self):
318 """Initiate the connection to the flight simulator."""
319 with self._requestCondition:
320 if not self._connectionRequested:
321 self._connectionRequested = True
322 self._requestCondition.notify()
323
324 def disconnect(self):
325 """Disconnect from the flight simulator."""
326 with self._requestCondition:
327 self._requests = []
328 if self._connectionRequested:
329 self._connectionRequested = False
330 self._requestCondition.notify()
331
332 def clearRequests(self):
333 """Clear the outstanding one-shot requests."""
334 with self._requestCondition:
335 self._requests = []
336
337 def run(self):
338 """Perform the operation of the thread."""
339 while True:
340 self._waitConnectionRequest()
341
342 if self._connect()>0:
343 self._handleConnection()
344
345 self._disconnect()
346
347 def _waitConnectionRequest(self):
348 """Wait for a connection request to arrive."""
349 with self._requestCondition:
350 while not self._connectionRequested:
351 self._requestCondition.wait()
352
353 def _connect(self, autoReconnection = False, attempts = 0):
354 """Try to connect to the flight simulator via FSUIPC
355
356 Returns True if the connection has been established, False if it was
357 not due to no longer requested.
358 """
359 while self._connectionRequested:
360 if attempts>=self.NUM_CONNECTATTEMPTS:
361 self._connectionRequested = False
362 if autoReconnection:
363 Handler._callSafe(lambda:
364 self._connectionListener.disconnected())
365 else:
366 Handler._callSafe(lambda:
367 self._connectionListener.connectionFailed())
368 return 0
369
370 try:
371 attempts += 1
372 pyuipc.open(pyuipc.SIM_ANY)
373 description = "(FSUIPC version: 0x%04x, library version: 0x%04x, FS version: %d)" % \
374 (pyuipc.fsuipc_version, pyuipc.lib_version,
375 pyuipc.fs_version)
376 if not autoReconnection:
377 fsType = const.SIM_MSFSX \
378 if pyuipc.fs_version == pyuipc.SIM_FSX \
379 else const.SIM_MSFS9
380
381 Handler._callSafe(lambda:
382 self._connectionListener.connected(fsType,
383 description))
384 self._connected = True
385 return attempts
386 except Exception, e:
387 print "fsuipc.Handler._connect: connection failed: " + str(e) + \
388 " (attempts: %d)" % (attempts,)
389 if attempts<self.NUM_CONNECTATTEMPTS:
390 time.sleep(self.CONNECT_INTERVAL)
391
392 def _handleConnection(self):
393 """Handle a living connection."""
394 with self._requestCondition:
395 while self._connectionRequested:
396 self._processRequests()
397 self._waitRequest()
398
399 def _waitRequest(self):
400 """Wait for the time of the next request.
401
402 Returns also, if the connection is no longer requested.
403
404 Should be called with the request condition lock held."""
405 while self._connectionRequested:
406 timeout = None
407 if self._periodicRequests:
408 self._periodicRequests.sort()
409 timeout = self._periodicRequests[0].nextFire - time.time()
410
411 if self._requests or \
412 (timeout is not None and timeout <= 0.0):
413 return
414
415 self._requestCondition.wait(timeout)
416
417 def _disconnect(self):
418 """Disconnect from the flight simulator."""
419 print "fsuipc.Handler._disconnect"
420 if self._connected:
421 pyuipc.close()
422 self._connected = False
423
424 def _processRequest(self, request, time, attempts):
425 """Process the given request.
426
427 If an exception occurs or invalid data is read too many times, we try
428 to reconnect.
429
430 This function returns only if the request has succeeded, or if a
431 connection is no longer requested.
432
433 This function is called with the request lock held, but is relased
434 whole processing the request and reconnecting."""
435 self._requestCondition.release()
436
437 #print "fsuipc.Handler._processRequest", request
438
439 needReconnect = False
440 try:
441 try:
442 if not request.process(time):
443 print "fsuipc.Handler._processRequest: FSUIPC returned invalid data too many times, reconnecting"
444 needReconnect = True
445 except Exception as e:
446 print "fsuipc.Handler._processRequest: FSUIPC connection failed (" + \
447 str(e) + "), reconnecting (attempts=%d)." % (attempts,)
448 needReconnect = True
449
450 if needReconnect:
451 with self._requestCondition:
452 self._requests.insert(0, request)
453 self._disconnect()
454 return self._connect(autoReconnection = True, attempts = attempts)
455 else:
456 return 0
457 finally:
458 self._requestCondition.acquire()
459
460 def _processRequests(self):
461 """Process any pending requests.
462
463 Will be called with the request lock held."""
464 attempts = 0
465 while self._connectionRequested and self._periodicRequests:
466 self._periodicRequests.sort()
467 request = self._periodicRequests[0]
468
469 t = time.time()
470
471 if request.nextFire>t:
472 break
473
474 attempts = self._processRequest(request, t, attempts)
475
476 while self._connectionRequested and self._requests:
477 request = self._requests[0]
478 del self._requests[0]
479
480 attempts = self._processRequest(request, None, attempts)
481
482 return self._connectionRequested
483
484#------------------------------------------------------------------------------
485
486class Simulator(object):
487 """The simulator class representing the interface to the flight simulator
488 via FSUIPC."""
489 # The basic data that should be queried all the time once we are connected
490 timeData = [ (0x0240, "H"), # Year
491 (0x023e, "H"), # Number of day in year
492 (0x023b, "b"), # UTC hour
493 (0x023c, "b"), # UTC minute
494 (0x023a, "b") ] # seconds
495
496 normalData = timeData + \
497 [ (0x3d00, -256), # The name of the current aircraft
498 (0x3c00, -256), # The path of the current AIR file
499 (0x1274, "h") ] # Text display mode
500
501 flareData1 = [ (0x023a, "b"), # Seconds of time
502 (0x31e4, "d"), # Radio altitude
503 (0x02c8, "d") ] # Vertical speed
504
505 flareStartData = [ (0x0e90, "H"), # Ambient wind speed
506 (0x0e92, "H"), # Ambient wind direction
507 (0x0e8a, "H") ] # Visibility
508
509 flareData2 = [ (0x023a, "b"), # Seconds of time
510 (0x0366, "H"), # On the ground
511 (0x02c8, "d"), # Vertical speed
512 (0x030c, "d"), # Touch-down rate
513 (0x02bc, "d"), # IAS
514 (0x0578, "d"), # Pitch
515 (0x057c, "d"), # Bank
516 (0x0580, "d") ] # Heading
517
518 TIME_SYNC_INTERVAL = 3.0
519
520 @staticmethod
521 def _getTimestamp(data):
522 """Convert the given data into a timestamp."""
523 timestamp = calendar.timegm(time.struct_time([data[0],
524 1, 1, 0, 0, 0, -1, 1, 0]))
525 timestamp += data[1] * 24 * 3600
526 timestamp += data[2] * 3600
527 timestamp += data[3] * 60
528 timestamp += data[4]
529
530 return timestamp
531
532 @staticmethod
533 def _appendHotkeyData(data, offset, hotkey):
534 """Append the data for the given hotkey to the given array, that is
535 intended to be passed to requestWrite call on the handler."""
536 data.append((offset + 0, "b", ord(hotkey.key)))
537
538 modifiers = 0
539 if hotkey.ctrl: modifiers |= 0x02
540 if hotkey.shift: modifiers |= 0x01
541 data.append((offset + 1, "b", modifiers))
542
543 data.append((offset + 2, "b", 0))
544
545 data.append((offset + 3, "b", 0))
546
547 def __init__(self, connectionListener, connectAttempts = -1,
548 connectInterval = 0.2):
549 """Construct the simulator.
550
551 The aircraft object passed must provide the following members:
552 - type: one of the AIRCRAFT_XXX constants from const.py
553 - modelChanged(aircraftName, modelName): called when the model handling
554 the aircraft has changed.
555 - handleState(aircraftState): handle the given state.
556 - flareStarted(windSpeed, windDirection, visibility, flareStart,
557 flareStartFS): called when the flare has
558 started. windSpeed is in knots, windDirection is in degrees and
559 visibility is in metres. flareStart and flareStartFS are two time
560 values expressed in seconds that can be used to calculate the flare
561 time.
562 - flareFinished(flareEnd, flareEndFS, tdRate, tdRateCalculatedByFS,
563 ias, pitch, bank, heading): called when the flare has
564 finished, i.e. the aircraft is on the ground. flareEnd and flareEndFS
565 are the two time values corresponding to the touchdown time. tdRate is
566 the touch-down rate, tdRateCalculatedBySim indicates if the data comes
567 from the simulator or was calculated by the adapter. The other data
568 are self-explanatory and expressed in their 'natural' units."""
569 self._fsType = None
570 self._aircraft = None
571
572 self._handler = Handler(self,
573 connectAttempts = connectAttempts,
574 connectInterval = connectInterval)
575 self._connectionListener = connectionListener
576 self._handler.start()
577
578 self._scroll = False
579
580 self._syncTime = False
581 self._nextSyncTime = -1
582
583 self._normalRequestID = None
584
585 self._monitoringRequested = False
586 self._monitoring = False
587
588 self._aircraftName = None
589 self._aircraftModel = None
590
591 self._flareRequestID = None
592 self._flareRates = []
593 self._flareStart = None
594 self._flareStartFS = None
595
596 self._hotkeyLock = threading.Lock()
597 self._hotkeys = None
598 self._hotkeySetID = 0
599 self._hotkeySetGeneration = 0
600 self._hotkeyOffets = None
601 self._hotkeyRequestID = None
602 self._hotkeyCallback = None
603
604 self._latin1decoder = codecs.getdecoder("iso-8859-1")
605 self._fuelCallback = None
606
607 def connect(self, aircraft):
608 """Initiate a connection to the simulator."""
609 self._aircraft = aircraft
610 self._aircraftName = None
611 self._aircraftModel = None
612 self._handler.connect()
613 if self._normalRequestID is None:
614 self._nextSyncTime = -1
615 self._startDefaultNormal()
616
617 def reconnect(self):
618 """Initiate a reconnection to the simulator.
619
620 It does not reset already set up data, just calls connect() on the
621 handler."""
622 self._handler.connect()
623
624 def requestZFW(self, callback):
625 """Send a request for the ZFW."""
626 self._handler.requestRead([(0x3bfc, "d")], self._handleZFW, extra = callback)
627
628 def requestWeights(self, callback):
629 """Request the following weights: DOW, ZFW, payload.
630
631 These values will be passed to the callback function in this order, as
632 separate arguments."""
633 self._handler.requestRead([(0x13fc, "d")], self._handlePayloadCount,
634 extra = callback)
635
636 def requestTime(self, callback):
637 """Request the time from the simulator."""
638 self._handler.requestRead(Simulator.timeData, self._handleTime,
639 extra = callback)
640
641 def startMonitoring(self):
642 """Start the periodic monitoring of the aircraft and pass the resulting
643 state to the aircraft object periodically."""
644 assert not self._monitoringRequested
645 self._monitoringRequested = True
646
647 def stopMonitoring(self):
648 """Stop the periodic monitoring of the aircraft."""
649 assert self._monitoringRequested
650 self._monitoringRequested = False
651
652 def startFlare(self):
653 """Start monitoring the flare time.
654
655 At present it is assumed to be called from the FSUIPC thread, hence no
656 protection."""
657 #self._aircraft.logger.debug("startFlare")
658 if self._flareRequestID is None:
659 self._flareRates = []
660 self._flareRequestID = self._handler.requestPeriodicRead(0.1,
661 Simulator.flareData1,
662 self._handleFlare1)
663
664 def cancelFlare(self):
665 """Cancel monitoring the flare time.
666
667 At present it is assumed to be called from the FSUIPC thread, hence no
668 protection."""
669 if self._flareRequestID is not None:
670 self._handler.clearPeriodic(self._flareRequestID)
671 self._flareRequestID = None
672
673 def sendMessage(self, message, duration = 3,
674 _disconnect = False):
675 """Send a message to the pilot via the simulator.
676
677 duration is the number of seconds to keep the message displayed."""
678
679 if self._scroll:
680 if duration==0: duration = -1
681 elif duration == 1: duration = -2
682 else: duration = -duration
683
684 data = [(0x3380, -1 - len(message), message),
685 (0x32fa, 'h', duration)]
686
687 #if _disconnect:
688 # print "fsuipc.Simulator.sendMessage(disconnect)", message
689
690 self._handler.requestWrite(data, self._handleMessageSent,
691 extra = _disconnect)
692
693 def getFuel(self, callback):
694 """Get the fuel information for the current model.
695
696 The callback will be called with a list of triplets with the following
697 items:
698 - the fuel tank identifier
699 - the current weight of the fuel in the tank (in kgs)
700 - the current total capacity of the tank (in kgs)."""
701 if self._aircraftModel is None:
702 self._fuelCallback = callback
703 else:
704 self._aircraftModel.getFuel(self._handler, callback)
705
706 def setFuelLevel(self, levels):
707 """Set the fuel level to the given ones.
708
709 levels is an array of two-tuples, where each tuple consists of the
710 following:
711 - the const.FUELTANK_XXX constant denoting the tank that must be set,
712 - the requested level of the fuel as a floating-point value between 0.0
713 and 1.0."""
714 if self._aircraftModel is not None:
715 self._aircraftModel.setFuelLevel(self._handler, levels)
716
717 def enableTimeSync(self):
718 """Enable the time synchronization."""
719 self._nextSyncTime = -1
720 self._syncTime = True
721
722 def disableTimeSync(self):
723 """Enable the time synchronization."""
724 self._syncTime = False
725 self._nextSyncTime = -1
726
727 def listenHotkeys(self, hotkeys, callback):
728 """Start listening to the given hotkeys.
729
730 callback is function expecting two arguments:
731 - the ID of the hotkey set as returned by this function,
732 - the list of the indexes of the hotkeys that were pressed."""
733 with self._hotkeyLock:
734 assert self._hotkeys is None
735
736 self._hotkeys = hotkeys
737 self._hotkeySetID += 1
738 self._hotkeySetGeneration = 0
739 self._hotkeyCallback = callback
740
741 self._handler.requestRead([(0x320c, "u")],
742 self._handleNumHotkeys,
743 (self._hotkeySetID,
744 self._hotkeySetGeneration))
745
746 return self._hotkeySetID
747
748 def clearHotkeys(self):
749 """Clear the current hotkey set.
750
751 Note that it is possible, that the callback function set either
752 previously or after calling this function by listenHotkeys() will be
753 called with data from the previous hotkey set.
754
755 Therefore it is recommended to store the hotkey set ID somewhere and
756 check that in the callback function. Right before calling
757 clearHotkeys(), this stored ID should be cleared so that the check
758 fails for sure."""
759 with self._hotkeyLock:
760 if self._hotkeys is not None:
761 self._hotkeys = None
762 self._hotkeySetID += 1
763 self._hotkeyCallback = None
764 self._clearHotkeyRequest()
765
766 def disconnect(self, closingMessage = None, duration = 3):
767 """Disconnect from the simulator."""
768 assert not self._monitoringRequested
769
770 print "fsuipc.Simulator.disconnect", closingMessage, duration
771
772 self._stopNormal()
773 self.clearHotkeys()
774 if closingMessage is None:
775 self._handler.disconnect()
776 else:
777 self.sendMessage(closingMessage, duration = duration,
778 _disconnect = True)
779
780 def connected(self, fsType, descriptor):
781 """Called when a connection has been established to the flight
782 simulator of the given type."""
783 self._fsType = fsType
784 with self._hotkeyLock:
785 if self._hotkeys is not None:
786 self._hotkeySetGeneration += 1
787
788 self._handler.requestRead([(0x320c, "u")],
789 self._handleNumHotkeys,
790 (self._hotkeySetID,
791 self._hotkeySetGeneration))
792 self._connectionListener.connected(fsType, descriptor)
793
794 def connectionFailed(self):
795 """Called when the connection could not be established."""
796 with self._hotkeyLock:
797 self._clearHotkeyRequest()
798 self._connectionListener.connectionFailed()
799
800 def disconnected(self):
801 """Called when a connection to the flight simulator has been broken."""
802 with self._hotkeyLock:
803 self._clearHotkeyRequest()
804 self._connectionListener.disconnected()
805
806 def _startDefaultNormal(self):
807 """Start the default normal periodic request."""
808 assert self._normalRequestID is None
809 self._normalRequestID = \
810 self._handler.requestPeriodicRead(1.0,
811 Simulator.normalData,
812 self._handleNormal,
813 validator = self._validateNormal)
814
815 def _stopNormal(self):
816 """Stop the normal period request."""
817 assert self._normalRequestID is not None
818 self._handler.clearPeriodic(self._normalRequestID)
819 self._normalRequestID = None
820 self._monitoring = False
821
822 def _validateNormal(self, data, extra):
823 """Validate the normal data."""
824 return data[0]!=0 and data[1]!=0 and len(data[5])>0 and len(data[6])>0
825
826 def _handleNormal(self, data, extra):
827 """Handle the reply to the normal request.
828
829 At the beginning the result consists the data for normalData. When
830 monitoring is started, it contains the result also for the
831 aircraft-specific values.
832 """
833 timestamp = Simulator._getTimestamp(data)
834
835 createdNewModel = self._setAircraftName(timestamp, data[5], data[6])
836 if self._fuelCallback is not None:
837 self._aircraftModel.getFuel(self._handler, self._fuelCallback)
838 self._fuelCallback = None
839
840 self._scroll = data[7]!=0
841
842 if self._monitoringRequested and not self._monitoring:
843 self._stopNormal()
844 self._startMonitoring()
845 elif self._monitoring and not self._monitoringRequested:
846 self._stopNormal()
847 self._startDefaultNormal()
848 elif self._monitoring and self._aircraftModel is not None and \
849 not createdNewModel:
850 aircraftState = self._aircraftModel.getAircraftState(self._aircraft,
851 timestamp, data)
852
853 self._checkTimeSync(aircraftState)
854
855 self._aircraft.handleState(aircraftState)
856
857 def _checkTimeSync(self, aircraftState):
858 """Check if we need to synchronize the FS time."""
859 if not self._syncTime or aircraftState.paused or \
860 self._flareRequestID is not None:
861 self._nextSyncTime = -1
862 return
863
864 now = time.time()
865 seconds = time.gmtime(now).tm_sec
866
867 if seconds>30 and seconds<59:
868 if self._nextSyncTime > (now - 0.49):
869 return
870
871 self._handler.requestWrite([(0x023a, "b", int(seconds))],
872 self._handleTimeSynced)
873
874 #print "Set the seconds to ", seconds
875
876 if self._nextSyncTime<0:
877 self._nextSyncTime = now
878
879 self._nextSyncTime += Simulator.TIME_SYNC_INTERVAL
880 else:
881 self._nextSyncTime = -1
882
883 def _handleTimeSynced(self, success, extra):
884 """Callback for the time sync result."""
885 pass
886
887 def _setAircraftName(self, timestamp, name, airPath):
888 """Set the name of the aicraft and if it is different from the
889 previous, create a new model for it.
890
891 If so, also notifty the aircraft about the change.
892
893 Return if a new model was created."""
894 aircraftName = (name, airPath)
895 if aircraftName==self._aircraftName:
896 return False
897
898 print "fsuipc.Simulator: new aircraft name and air file path: %s, %s" % \
899 (name, airPath)
900
901 self._aircraftName = aircraftName
902 needNew = self._aircraftModel is None
903 needNew = needNew or\
904 not self._aircraftModel.doesHandle(self._aircraft, aircraftName)
905 if not needNew:
906 specialModel = AircraftModel.findSpecial(self._aircraft, aircraftName)
907 needNew = specialModel is not None and \
908 specialModel is not self._aircraftModel.__class__
909
910 if needNew:
911 self._setAircraftModel(AircraftModel.create(self._aircraft, aircraftName))
912
913
914 self._aircraft.modelChanged(timestamp, self._latin1decoder(name)[0],
915 self._aircraftModel.name)
916
917 return needNew
918
919 def _setAircraftModel(self, model):
920 """Set a new aircraft model.
921
922 It will be queried for the data to monitor and the monitoring request
923 will be replaced by a new one."""
924 self._aircraftModel = model
925
926 if self._monitoring:
927 self._stopNormal()
928 self._startMonitoring()
929
930 def _startMonitoring(self):
931 """Start monitoring with the current aircraft model."""
932 data = Simulator.normalData[:]
933 self._aircraftModel.addMonitoringData(data, self._fsType)
934
935 self._normalRequestID = \
936 self._handler.requestPeriodicRead(1.0, data,
937 self._handleNormal,
938 validator = self._validateNormal)
939 self._monitoring = True
940
941 def _addFlareRate(self, data):
942 """Append a flare rate to the list of last rates."""
943 if len(self._flareRates)>=3:
944 del self._flareRates[0]
945 self._flareRates.append(Handler.fsuipc2VS(data))
946
947 def _handleFlare1(self, data, normal):
948 """Handle the first stage of flare monitoring."""
949 #self._aircraft.logger.debug("handleFlare1: " + str(data))
950 if Handler.fsuipc2radioAltitude(data[1])<=50.0:
951 self._flareStart = time.time()
952 self._flareStartFS = data[0]
953 self._handler.clearPeriodic(self._flareRequestID)
954 self._flareRequestID = \
955 self._handler.requestPeriodicRead(0.1,
956 Simulator.flareData2,
957 self._handleFlare2)
958 self._handler.requestRead(Simulator.flareStartData,
959 self._handleFlareStart)
960
961 self._addFlareRate(data[2])
962
963 def _handleFlareStart(self, data, extra):
964 """Handle the data need to notify the aircraft about the starting of
965 the flare."""
966 #self._aircraft.logger.debug("handleFlareStart: " + str(data))
967 if data is not None:
968 windDirection = data[1]*360.0/65536.0
969 if windDirection<0.0: windDirection += 360.0
970 self._aircraft.flareStarted(data[0], windDirection,
971 data[2]*1609.344/100.0,
972 self._flareStart, self._flareStartFS)
973
974 def _handleFlare2(self, data, normal):
975 """Handle the first stage of flare monitoring."""
976 #self._aircraft.logger.debug("handleFlare2: " + str(data))
977 if data[1]!=0:
978 flareEnd = time.time()
979 self._handler.clearPeriodic(self._flareRequestID)
980 self._flareRequestID = None
981
982 flareEndFS = data[0]
983 if flareEndFS<self._flareStartFS:
984 flareEndFS += 60
985
986 tdRate = Handler.fsuipc2VS(data[3])
987 tdRateCalculatedByFS = True
988 if tdRate==0 or tdRate>1000.0 or tdRate<-1000.0:
989 tdRate = min(self._flareRates)
990 tdRateCalculatedByFS = False
991
992 self._aircraft.flareFinished(flareEnd, flareEndFS,
993 tdRate, tdRateCalculatedByFS,
994 Handler.fsuipc2IAS(data[4]),
995 Handler.fsuipc2Degrees(data[5]),
996 Handler.fsuipc2Degrees(data[6]),
997 Handler.fsuipc2PositiveDegrees(data[7]))
998 else:
999 self._addFlareRate(data[2])
1000
1001 def _handleZFW(self, data, callback):
1002 """Callback for a ZFW retrieval request."""
1003 zfw = data[0] * const.LBSTOKG / 256.0
1004 callback(zfw)
1005
1006 def _handleTime(self, data, callback):
1007 """Callback for a time retrieval request."""
1008 callback(Simulator._getTimestamp(data))
1009
1010 def _handlePayloadCount(self, data, callback):
1011 """Callback for the payload count retrieval request."""
1012 payloadCount = data[0]
1013 data = [(0x3bfc, "d"), (0x30c0, "f")]
1014 for i in range(0, payloadCount):
1015 data.append((0x1400 + i*48, "f"))
1016
1017 self._handler.requestRead(data, self._handleWeights,
1018 extra = callback)
1019
1020 def _handleWeights(self, data, callback):
1021 """Callback for the weights retrieval request."""
1022 zfw = data[0] * const.LBSTOKG / 256.0
1023 grossWeight = data[1] * const.LBSTOKG
1024 payload = sum(data[2:]) * const.LBSTOKG
1025 dow = zfw - payload
1026 callback(dow, payload, zfw, grossWeight)
1027
1028 def _handleMessageSent(self, success, disconnect):
1029 """Callback for a message sending request."""
1030 #print "fsuipc.Simulator._handleMessageSent", disconnect
1031 if disconnect:
1032 self._handler.disconnect()
1033
1034 def _handleNumHotkeys(self, data, (id, generation)):
1035 """Handle the result of the query of the number of hotkeys"""
1036 with self._hotkeyLock:
1037 if id==self._hotkeySetID and generation==self._hotkeySetGeneration:
1038 numHotkeys = data[0]
1039 print "fsuipc.Simulator._handleNumHotkeys: numHotkeys:", numHotkeys
1040 data = [(0x3210 + i*4, "d") for i in range(0, numHotkeys)]
1041 self._handler.requestRead(data, self._handleHotkeyTable,
1042 (id, generation))
1043
1044 def _setupHotkeys(self, data):
1045 """Setup the hiven hotkeys and return the data to be written.
1046
1047 If there were hotkeys set previously, they are reused as much as
1048 possible. Any of them not reused will be cleared."""
1049 hotkeys = self._hotkeys
1050 numHotkeys = len(hotkeys)
1051
1052 oldHotkeyOffsets = set([] if self._hotkeyOffets is None else
1053 self._hotkeyOffets)
1054
1055 self._hotkeyOffets = []
1056 numOffsets = 0
1057
1058 while oldHotkeyOffsets:
1059 offset = oldHotkeyOffsets.pop()
1060 self._hotkeyOffets.append(offset)
1061 numOffsets += 1
1062
1063 if numOffsets>=numHotkeys:
1064 break
1065
1066 for i in range(0, len(data)):
1067 if numOffsets>=numHotkeys:
1068 break
1069
1070 if data[i]==0:
1071 self._hotkeyOffets.append(0x3210 + i*4)
1072 numOffsets += 1
1073
1074 writeData = []
1075 for i in range(0, numOffsets):
1076 Simulator._appendHotkeyData(writeData,
1077 self._hotkeyOffets[i],
1078 hotkeys[i])
1079
1080 for offset in oldHotkeyOffsets:
1081 writeData.append((offset, "u", long(0)))
1082
1083 return writeData
1084
1085 def _handleHotkeyTable(self, data, (id, generation)):
1086 """Handle the result of the query of the hotkey table."""
1087 with self._hotkeyLock:
1088 if id==self._hotkeySetID and generation==self._hotkeySetGeneration:
1089 writeData = self._setupHotkeys(data)
1090 self._handler.requestWrite(writeData,
1091 self._handleHotkeysWritten,
1092 (id, generation))
1093
1094 def _handleHotkeysWritten(self, success, (id, generation)):
1095 """Handle the result of the hotkeys having been written."""
1096 with self._hotkeyLock:
1097 if success and id==self._hotkeySetID and \
1098 generation==self._hotkeySetGeneration:
1099 data = [(offset + 3, "b") for offset in self._hotkeyOffets]
1100
1101 self._hotkeyRequestID = \
1102 self._handler.requestPeriodicRead(0.5, data,
1103 self._handleHotkeys,
1104 (id, generation))
1105
1106 def _handleHotkeys(self, data, (id, generation)):
1107 """Handle the hotkeys."""
1108 with self._hotkeyLock:
1109 if id!=self._hotkeySetID or generation!=self._hotkeySetGeneration:
1110 return
1111
1112 callback = self._hotkeyCallback
1113 offsets = self._hotkeyOffets
1114
1115 hotkeysPressed = []
1116 for i in range(0, len(data)):
1117 if data[i]!=0:
1118 hotkeysPressed.append(i)
1119
1120 if hotkeysPressed:
1121 data = []
1122 for index in hotkeysPressed:
1123 data.append((offsets[index]+3, "b", int(0)))
1124 self._handler.requestWrite(data, self._handleHotkeysCleared)
1125
1126 callback(id, hotkeysPressed)
1127
1128 def _handleHotkeysCleared(self, sucess, extra):
1129 """Callback for the hotkey-clearing write request."""
1130
1131 def _clearHotkeyRequest(self):
1132 """Clear the hotkey request in the handler if there is any."""
1133 if self._hotkeyRequestID is not None:
1134 self._handler.clearPeriodic(self._hotkeyRequestID)
1135 self._hotkeyRequestID = None
1136
1137#------------------------------------------------------------------------------
1138
1139class AircraftModel(object):
1140 """Base class for the aircraft models.
1141
1142 Aircraft models handle the data arriving from FSUIPC and turn it into an
1143 object describing the aircraft's state."""
1144 monitoringData = [("paused", 0x0264, "H"),
1145 ("latitude", 0x0560, "l"),
1146 ("longitude", 0x0568, "l"),
1147 ("frozen", 0x3364, "H"),
1148 ("replay", 0x0628, "d"),
1149 ("slew", 0x05dc, "H"),
1150 ("overspeed", 0x036d, "b"),
1151 ("stalled", 0x036c, "b"),
1152 ("onTheGround", 0x0366, "H"),
1153 ("zfw", 0x3bfc, "d"),
1154 ("grossWeight", 0x30c0, "f"),
1155 ("heading", 0x0580, "d"),
1156 ("pitch", 0x0578, "d"),
1157 ("bank", 0x057c, "d"),
1158 ("ias", 0x02bc, "d"),
1159 ("mach", 0x11c6, "H"),
1160 ("groundSpeed", 0x02b4, "d"),
1161 ("vs", 0x02c8, "d"),
1162 ("radioAltitude", 0x31e4, "d"),
1163 ("altitude", 0x0570, "l"),
1164 ("gLoad", 0x11ba, "H"),
1165 ("flapsControl", 0x0bdc, "d"),
1166 ("flapsLeft", 0x0be0, "d"),
1167 ("flapsRight", 0x0be4, "d"),
1168 ("lights", 0x0d0c, "H"),
1169 ("pitot", 0x029c, "b"),
1170 ("parking", 0x0bc8, "H"),
1171 ("gearControl", 0x0be8, "d"),
1172 ("noseGear", 0x0bec, "d"),
1173 ("spoilersArmed", 0x0bcc, "d"),
1174 ("spoilers", 0x0bd0, "d"),
1175 ("altimeter", 0x0330, "H"),
1176 ("nav1", 0x0350, "H"),
1177 ("nav2", 0x0352, "H"),
1178 ("squawk", 0x0354, "H"),
1179 ("windSpeed", 0x0e90, "H"),
1180 ("windDirection", 0x0e92, "H"),
1181 ("visibility", 0x0e8a, "H"),
1182 ("cog", 0x2ef8, "f")]
1183
1184 specialModels = []
1185
1186 @staticmethod
1187 def registerSpecial(clazz):
1188 """Register the given class as a special model."""
1189 AircraftModel.specialModels.append(clazz)
1190
1191 @staticmethod
1192 def findSpecial(aircraft, aircraftName):
1193 for specialModel in AircraftModel.specialModels:
1194 if specialModel.doesHandle(aircraft, aircraftName):
1195 return specialModel
1196 return None
1197
1198 @staticmethod
1199 def create(aircraft, aircraftName):
1200 """Create the model for the given aircraft name, and notify the
1201 aircraft about it."""
1202 specialModel = AircraftModel.findSpecial(aircraft, aircraftName)
1203 if specialModel is not None:
1204 return specialModel()
1205 if aircraft.type in _genericModels:
1206 return _genericModels[aircraft.type]()
1207 else:
1208 return GenericModel()
1209
1210 @staticmethod
1211 def convertBCD(data, length):
1212 """Convert a data item encoded as BCD into a string of the given number
1213 of digits."""
1214 bcd = ""
1215 for i in range(0, length):
1216 digit = chr(ord('0') + (data&0x0f))
1217 data >>= 4
1218 bcd = digit + bcd
1219 return bcd
1220
1221 @staticmethod
1222 def convertFrequency(data):
1223 """Convert the given frequency data to a string."""
1224 bcd = AircraftModel.convertBCD(data, 4)
1225 return "1" + bcd[0:2] + "." + bcd[2:4]
1226
1227 def __init__(self, flapsNotches):
1228 """Construct the aircraft model.
1229
1230 flapsNotches is a list of degrees of flaps that are available on the aircraft."""
1231 self._flapsNotches = flapsNotches
1232
1233 @property
1234 def name(self):
1235 """Get the name for this aircraft model."""
1236 return "FSUIPC/Generic"
1237
1238 def doesHandle(self, aircraft, aircraftName):
1239 """Determine if the model handles the given aircraft name.
1240
1241 This default implementation returns False."""
1242 return False
1243
1244 def _addOffsetWithIndexMember(self, dest, offset, type, attrName = None):
1245 """Add the given FSUIPC offset and type to the given array and a member
1246 attribute with the given name."""
1247 dest.append((offset, type))
1248 if attrName is not None:
1249 setattr(self, attrName, len(dest)-1)
1250
1251 def _addDataWithIndexMembers(self, dest, prefix, data):
1252 """Add FSUIPC data to the given array and also corresponding index
1253 member variables with the given prefix.
1254
1255 data is a list of triplets of the following items:
1256 - the name of the data item. The index member variable will have a name
1257 created by prepending the given prefix to this name.
1258 - the FSUIPC offset
1259 - the FSUIPC type
1260
1261 The latter two items will be appended to dest."""
1262 for (name, offset, type) in data:
1263 self._addOffsetWithIndexMember(dest, offset, type, prefix + name)
1264
1265 def addMonitoringData(self, data, fsType):
1266 """Add the model-specific monitoring data to the given array."""
1267 self._addDataWithIndexMembers(data, "_monidx_",
1268 AircraftModel.monitoringData)
1269
1270 def getAircraftState(self, aircraft, timestamp, data):
1271 """Get an aircraft state object for the given monitoring data."""
1272 state = fs.AircraftState()
1273
1274 state.timestamp = timestamp
1275
1276 state.latitude = data[self._monidx_latitude] * \
1277 90.0 / 10001750.0 / 65536.0 / 65536.0
1278
1279 state.longitude = data[self._monidx_longitude] * \
1280 360.0 / 65536.0 / 65536.0 / 65536.0 / 65536.0
1281 if state.longitude>180.0: state.longitude = 360.0 - state.longitude
1282
1283 state.paused = data[self._monidx_paused]!=0 or \
1284 data[self._monidx_frozen]!=0 or \
1285 data[self._monidx_replay]!=0
1286 state.trickMode = data[self._monidx_slew]!=0
1287
1288 state.overspeed = data[self._monidx_overspeed]!=0
1289 state.stalled = data[self._monidx_stalled]!=0
1290 state.onTheGround = data[self._monidx_onTheGround]!=0
1291
1292 state.zfw = data[self._monidx_zfw] * const.LBSTOKG / 256.0
1293 state.grossWeight = data[self._monidx_grossWeight] * const.LBSTOKG
1294
1295 state.heading = Handler.fsuipc2PositiveDegrees(data[self._monidx_heading])
1296
1297 state.pitch = Handler.fsuipc2Degrees(data[self._monidx_pitch])
1298 state.bank = Handler.fsuipc2Degrees(data[self._monidx_bank])
1299
1300 state.ias = Handler.fsuipc2IAS(data[self._monidx_ias])
1301 state.mach = data[self._monidx_mach] / 20480.0
1302 state.groundSpeed = data[self._monidx_groundSpeed]* 3600.0/65536.0/1852.0
1303 state.vs = Handler.fsuipc2VS(data[self._monidx_vs])
1304
1305 state.radioAltitude = \
1306 Handler.fsuipc2radioAltitude(data[self._monidx_radioAltitude])
1307 state.altitude = data[self._monidx_altitude]/const.FEETTOMETRES/65536.0/65536.0
1308
1309 state.gLoad = data[self._monidx_gLoad] / 625.0
1310
1311 numNotchesM1 = len(self._flapsNotches) - 1
1312 flapsIncrement = 16383 / numNotchesM1
1313 flapsControl = data[self._monidx_flapsControl]
1314 flapsIndex = flapsControl / flapsIncrement
1315 if flapsIndex < numNotchesM1:
1316 if (flapsControl - (flapsIndex*flapsIncrement) >
1317 (flapsIndex+1)*flapsIncrement - flapsControl):
1318 flapsIndex += 1
1319 state.flapsSet = self._flapsNotches[flapsIndex]
1320
1321 flapsLeft = data[self._monidx_flapsLeft]
1322 state.flaps = self._flapsNotches[-1]*flapsLeft/16383.0
1323
1324 lights = data[self._monidx_lights]
1325
1326 state.navLightsOn = (lights&0x01) != 0
1327 state.antiCollisionLightsOn = (lights&0x02) != 0
1328 state.landingLightsOn = (lights&0x04) != 0
1329 state.strobeLightsOn = (lights&0x10) != 0
1330
1331 state.pitotHeatOn = data[self._monidx_pitot]!=0
1332
1333 state.parking = data[self._monidx_parking]!=0
1334
1335 state.gearControlDown = data[self._monidx_gearControl]==16383
1336 state.gearsDown = data[self._monidx_noseGear]==16383
1337
1338 state.spoilersArmed = data[self._monidx_spoilersArmed]!=0
1339
1340 spoilers = data[self._monidx_spoilers]
1341 if spoilers<=4800:
1342 state.spoilersExtension = 0.0
1343 else:
1344 state.spoilersExtension = (spoilers - 4800) * 100.0 / (16383 - 4800)
1345
1346 state.altimeter = data[self._monidx_altimeter] / 16.0
1347
1348 state.nav1 = AircraftModel.convertFrequency(data[self._monidx_nav1])
1349 state.nav2 = AircraftModel.convertFrequency(data[self._monidx_nav2])
1350 state.squawk = AircraftModel.convertBCD(data[self._monidx_squawk], 4)
1351
1352 state.windSpeed = data[self._monidx_windSpeed]
1353 state.windDirection = data[self._monidx_windDirection]*360.0/65536.0
1354 if state.windDirection<0.0: state.windDirection += 360.0
1355
1356 state.visibility = data[self._monidx_visibility]*1609.344/100.0
1357
1358 state.cog = data[self._monidx_cog]
1359
1360 return state
1361
1362#------------------------------------------------------------------------------
1363
1364class GenericAircraftModel(AircraftModel):
1365 """A generic aircraft model that can handle the fuel levels, the N1 or RPM
1366 values and some other common parameters in a generic way."""
1367
1368 def __init__(self, flapsNotches, fuelTanks, numEngines, isN1 = True):
1369 """Construct the generic aircraft model with the given data.
1370
1371 flapsNotches is an array of how much degrees the individual flaps
1372 notches mean.
1373
1374 fuelTanks is an array of const.FUELTANK_XXX constants about the
1375 aircraft's fuel tanks. They will be converted to offsets.
1376
1377 numEngines is the number of engines the aircraft has.
1378
1379 isN1 determines if the engines have an N1 value or an RPM value
1380 (e.g. pistons)."""
1381 super(GenericAircraftModel, self).__init__(flapsNotches = flapsNotches)
1382
1383 self._fuelTanks = fuelTanks
1384 self._fuelStartIndex = None
1385 self._numEngines = numEngines
1386 self._engineStartIndex = None
1387 self._isN1 = isN1
1388
1389 def doesHandle(self, aircraft, aircraftName):
1390 """Determine if the model handles the given aircraft name.
1391
1392 This implementation returns True."""
1393 return True
1394
1395 def addMonitoringData(self, data, fsType):
1396 """Add the model-specific monitoring data to the given array."""
1397 super(GenericAircraftModel, self).addMonitoringData(data, fsType)
1398
1399 self._fuelStartIndex = self._addFuelOffsets(data, "_monidx_fuelWeight")
1400
1401 self._engineStartIndex = len(data)
1402 for i in range(0, self._numEngines):
1403 self._addOffsetWithIndexMember(data, 0x088c + i * 0x98, "h") # throttle lever
1404 if self._isN1:
1405 self._addOffsetWithIndexMember(data, 0x2000 + i * 0x100, "f") # N1
1406 else:
1407 self._addOffsetWithIndexMember(data, 0x0898 + i * 0x98, "H") # RPM
1408 self._addOffsetWithIndexMember(data, 0x08c8 + i * 0x98, "H") # RPM scaler
1409
1410 def getAircraftState(self, aircraft, timestamp, data):
1411 """Get the aircraft state.
1412
1413 Get it from the parent, and then add the data about the fuel levels and
1414 the engine parameters."""
1415 state = super(GenericAircraftModel, self).getAircraftState(aircraft,
1416 timestamp,
1417 data)
1418
1419 (state.fuel, state.totalFuel) = \
1420 self._convertFuelData(data, index = self._monidx_fuelWeight)
1421
1422 state.n1 = [] if self._isN1 else None
1423 state.rpm = None if self._isN1 else []
1424 itemsPerEngine = 2 if self._isN1 else 3
1425
1426 state.reverser = []
1427 for i in range(self._engineStartIndex,
1428 self._engineStartIndex +
1429 itemsPerEngine*self._numEngines,
1430 itemsPerEngine):
1431 state.reverser.append(data[i]<0)
1432 if self._isN1:
1433 state.n1.append(data[i+1])
1434 else:
1435 state.rpm.append(data[i+1] * data[i+2]/65536.0)
1436
1437 return state
1438
1439 def getFuel(self, handler, callback):
1440 """Get the fuel information for this model.
1441
1442 See Simulator.getFuel for more information. This
1443 implementation simply queries the fuel tanks given to the
1444 constructor."""
1445 data = []
1446 self._addFuelOffsets(data)
1447
1448 handler.requestRead(data, self._handleFuelRetrieved,
1449 extra = callback)
1450
1451 def setFuelLevel(self, handler, levels):
1452 """Set the fuel level.
1453
1454 See the description of Simulator.setFuelLevel. This
1455 implementation simply sets the fuel tanks as given."""
1456 data = []
1457 for (tank, level) in levels:
1458 offset = _tank2offset[tank]
1459 value = long(level * 128.0 * 65536.0)
1460 data.append( (offset, "u", value) )
1461
1462 handler.requestWrite(data, self._handleFuelWritten)
1463
1464 def _addFuelOffsets(self, data, weightIndexName = None):
1465 """Add the fuel offsets to the given data array.
1466
1467 If weightIndexName is not None, it will be the name of the
1468 fuel weight index.
1469
1470 Returns the index of the first fuel tank's data."""
1471 self._addOffsetWithIndexMember(data, 0x0af4, "H", weightIndexName)
1472
1473 fuelStartIndex = len(data)
1474 for tank in self._fuelTanks:
1475 offset = _tank2offset[tank]
1476 self._addOffsetWithIndexMember(data, offset, "u") # tank level
1477 self._addOffsetWithIndexMember(data, offset+4, "u") # tank capacity
1478
1479 return fuelStartIndex
1480
1481 def _convertFuelData(self, data, index = 0, addCapacities = False):
1482 """Convert the given data into a fuel info list.
1483
1484 The list consists of two or three-tuples of the following
1485 items:
1486 - the fuel tank ID,
1487 - the amount of the fuel in kg,
1488 - if addCapacities is True, the total capacity of the tank."""
1489 fuelWeight = data[index] / 256.0
1490 index += 1
1491
1492 result = []
1493 totalFuel = 0
1494 for fuelTank in self._fuelTanks:
1495 capacity = data[index+1] * fuelWeight * const.LBSTOKG
1496 amount = data[index] * capacity / 128.0 / 65536.0
1497 index += 2
1498
1499 result.append( (fuelTank, amount, capacity) if addCapacities
1500 else (fuelTank, amount))
1501 totalFuel += amount
1502
1503 return (result, totalFuel)
1504
1505 def _handleFuelRetrieved(self, data, callback):
1506 """Callback for a fuel retrieval request."""
1507 (fuelData, _totalFuel) = self._convertFuelData(data,
1508 addCapacities = True)
1509 callback(fuelData)
1510
1511 def _handleFuelWritten(self, success, extra):
1512 """Callback for a fuel setting request."""
1513 pass
1514
1515#------------------------------------------------------------------------------
1516
1517class GenericModel(GenericAircraftModel):
1518 """Generic aircraft model for an unknown type."""
1519 def __init__(self):
1520 """Construct the model."""
1521 super(GenericModel, self). \
1522 __init__(flapsNotches = [0, 10, 20, 30],
1523 fuelTanks = [const.FUELTANK_LEFT, const.FUELTANK_RIGHT],
1524 numEngines = 2)
1525
1526 @property
1527 def name(self):
1528 """Get the name for this aircraft model."""
1529 return "FSUIPC/Generic"
1530
1531#------------------------------------------------------------------------------
1532
1533class B737Model(GenericAircraftModel):
1534 """Generic model for the Boeing 737 Classing and NG aircraft."""
1535 fuelTanks = [const.FUELTANK_LEFT, const.FUELTANK_CENTRE, const.FUELTANK_RIGHT]
1536
1537 def __init__(self):
1538 """Construct the model."""
1539 super(B737Model, self). \
1540 __init__(flapsNotches = [0, 1, 2, 5, 10, 15, 25, 30, 40],
1541 fuelTanks = B737Model.fuelTanks,
1542 numEngines = 2)
1543
1544 @property
1545 def name(self):
1546 """Get the name for this aircraft model."""
1547 return "FSUIPC/Generic Boeing 737"
1548
1549#------------------------------------------------------------------------------
1550
1551class PMDGBoeing737NGModel(B737Model):
1552 """A model handler for the PMDG Boeing 737NG model."""
1553 @staticmethod
1554 def doesHandle(aircraft, (name, airPath)):
1555 """Determine if this model handler handles the aircraft with the given
1556 name."""
1557 return aircraft.type in [const.AIRCRAFT_B736,
1558 const.AIRCRAFT_B737,
1559 const.AIRCRAFT_B738,
1560 const.AIRCRAFT_B738C] and \
1561 (name.find("PMDG")!=-1 or airPath.find("PMDG")!=-1) and \
1562 (name.find("737")!=-1 or airPath.find("737")!=-1) and \
1563 (name.find("600")!=-1 or airPath.find("600")!=-1 or \
1564 name.find("700")!=-1 or airPath.find("700")!=-1 or \
1565 name.find("800")!=-1 or airPath.find("800")!=-1 or \
1566 name.find("900")!=-1 or airPath.find("900")!=-1)
1567
1568 @property
1569 def name(self):
1570 """Get the name for this aircraft model."""
1571 return "FSUIPC/PMDG Boeing 737NG(X)"
1572
1573 def addMonitoringData(self, data, fsType):
1574 """Add the model-specific monitoring data to the given array."""
1575 self._fsType = fsType
1576
1577 super(PMDGBoeing737NGModel, self).addMonitoringData(data, fsType)
1578
1579 self._addOffsetWithIndexMember(data, 0x6202, "b", "_pmdgidx_switches")
1580
1581 if fsType==const.SIM_MSFSX:
1582 print "FSX detected, adding position lights switch offset"
1583 self._addOffsetWithIndexMember(data, 0x6500, "b",
1584 "_pmdgidx_lts_positionsw")
1585
1586 def getAircraftState(self, aircraft, timestamp, data):
1587 """Get the aircraft state.
1588
1589 Get it from the parent, and then check some PMDG-specific stuff."""
1590 state = super(PMDGBoeing737NGModel, self).getAircraftState(aircraft,
1591 timestamp,
1592 data)
1593 if data[self._pmdgidx_switches]&0x01==0x01:
1594 state.altimeter = 1013.25
1595
1596 if self._fsType==const.SIM_MSFSX:
1597 state.strobeLightsOn = data[self._pmdgidx_lts_positionsw]==0x02
1598
1599 return state
1600
1601#------------------------------------------------------------------------------
1602
1603class B767Model(GenericAircraftModel):
1604 """Generic model for the Boeing 767 aircraft."""
1605 fuelTanks = [const.FUELTANK_LEFT, const.FUELTANK_CENTRE, const.FUELTANK_RIGHT]
1606
1607 def __init__(self):
1608 """Construct the model."""
1609 super(B767Model, self). \
1610 __init__(flapsNotches = [0, 1, 5, 15, 20, 25, 30],
1611 fuelTanks = Boeing767Model.fuelTanks,
1612 numEngines = 2)
1613
1614 @property
1615 def name(self):
1616 """Get the name for this aircraft model."""
1617 return "FSUIPC/Generic Boeing 767"
1618
1619#------------------------------------------------------------------------------
1620
1621class DH8DModel(GenericAircraftModel):
1622 """Generic model for the Bombardier Dash 8-Q400 aircraft."""
1623 fuelTanks = [const.FUELTANK_LEFT, const.FUELTANK_RIGHT]
1624
1625 def __init__(self):
1626 """Construct the model."""
1627 super(DH8DModel, self). \
1628 __init__(flapsNotches = [0, 5, 10, 15, 35],
1629 fuelTanks = DH8DModel.fuelTanks,
1630 numEngines = 2)
1631
1632 @property
1633 def name(self):
1634 """Get the name for this aircraft model."""
1635 return "FSUIPC/Generic Bombardier Dash 8-Q400"
1636
1637#------------------------------------------------------------------------------
1638
1639class DreamwingsDH8DModel(DH8DModel):
1640 """Model handler for the Dreamwings Dash 8-Q400."""
1641 @staticmethod
1642 def doesHandle(aircraft, (name, airPath)):
1643 """Determine if this model handler handles the aircraft with the given
1644 name."""
1645 return aircraft.type==const.AIRCRAFT_DH8D and \
1646 (name.find("Dreamwings")!=-1 or airPath.find("Dreamwings")!=-1) and \
1647 (name.find("Dash")!=-1 or airPath.find("Dash")!=-1) and \
1648 (name.find("Q400")!=-1 or airPath.find("Q400")!=-1) and \
1649 airPath.find("Dash8Q400")!=-1
1650
1651 @property
1652 def name(self):
1653 """Get the name for this aircraft model."""
1654 return "FSUIPC/Dreamwings Bombardier Dash 8-Q400"
1655
1656 def getAircraftState(self, aircraft, timestamp, data):
1657 """Get the aircraft state.
1658
1659 Get it from the parent, and then invert the pitot heat state."""
1660 state = super(DreamwingsDH8DModel, self).getAircraftState(aircraft,
1661 timestamp,
1662 data)
1663 state.pitotHeatOn = not state.pitotHeatOn
1664
1665 return state
1666
1667#------------------------------------------------------------------------------
1668
1669class CRJ2Model(GenericAircraftModel):
1670 """Generic model for the Bombardier CRJ-200 aircraft."""
1671 fuelTanks = [const.FUELTANK_LEFT, const.FUELTANK_CENTRE, const.FUELTANK_RIGHT]
1672
1673 def __init__(self):
1674 """Construct the model."""
1675 super(CRJ2Model, self). \
1676 __init__(flapsNotches = [0, 8, 20, 30, 45],
1677 fuelTanks = CRJ2Model.fuelTanks,
1678 numEngines = 2)
1679
1680 @property
1681 def name(self):
1682 """Get the name for this aircraft model."""
1683 return "FSUIPC/Generic Bombardier CRJ-200"
1684
1685#------------------------------------------------------------------------------
1686
1687class F70Model(GenericAircraftModel):
1688 """Generic model for the Fokker F70 aircraft."""
1689 fuelTanks = [const.FUELTANK_LEFT, const.FUELTANK_CENTRE, const.FUELTANK_RIGHT]
1690
1691 def __init__(self):
1692 """Construct the model."""
1693 super(F70Model, self). \
1694 __init__(flapsNotches = [0, 8, 15, 25, 42],
1695 fuelTanks = F70Model.fuelTanks,
1696 numEngines = 2)
1697
1698 @property
1699 def name(self):
1700 """Get the name for this aircraft model."""
1701 return "FSUIPC/Generic Fokker 70"
1702
1703#------------------------------------------------------------------------------
1704
1705class DAF70Model(F70Model):
1706 """Model for the Digital Aviation F70 implementation on FS9."""
1707 @staticmethod
1708 def doesHandle(aircraft, (name, airPath)):
1709 """Determine if this model handler handles the aircraft with the given
1710 name."""
1711 return aircraft.type == const.AIRCRAFT_F70 and \
1712 (airPath.endswith("fokker70_2k4_v4.1.air") or
1713 airPath.endswith("fokker70_2k4_v4.3.air"))
1714
1715 @property
1716 def name(self):
1717 """Get the name for this aircraft model."""
1718 return "FSUIPC/Digital Aviation Fokker 70"
1719
1720 def getAircraftState(self, aircraft, timestamp, data):
1721 """Get the aircraft state.
1722
1723 Get it from the parent, and then invert the pitot heat state."""
1724 state = super(DAF70Model, self).getAircraftState(aircraft,
1725 timestamp,
1726 data)
1727 state.landingLightsOn = None
1728 state.nav2 = None
1729
1730 return state
1731
1732#------------------------------------------------------------------------------
1733
1734class DC3Model(GenericAircraftModel):
1735 """Generic model for the Lisunov Li-2 (DC-3) aircraft."""
1736 fuelTanks = [const.FUELTANK_LEFT, const.FUELTANK_CENTRE,
1737 const.FUELTANK_RIGHT]
1738 # fuelTanks = [const.FUELTANK_LEFT_AUX, const.FUELTANK_LEFT,
1739 # const.FUELTANK_RIGHT, const.FUELTANK_RIGHT_AUX]
1740
1741 def __init__(self):
1742 """Construct the model."""
1743 super(DC3Model, self). \
1744 __init__(flapsNotches = [0, 15, 30, 45],
1745 fuelTanks = DC3Model.fuelTanks,
1746 numEngines = 2, isN1 = False)
1747 self._leftLevel = 0.0
1748 self._rightLevel = 0.0
1749
1750 @property
1751 def name(self):
1752 """Get the name for this aircraft model."""
1753 return "FSUIPC/Generic Lisunov Li-2 (DC-3)"
1754
1755 def _convertFuelData(self, data, index = 0, addCapacities = False):
1756 """Convert the given data into a fuel info list.
1757
1758 It assumes to receive the 3 fuel tanks as seen above (left,
1759 centre and right) and converts it to left aux, left, right,
1760 and right aux. The amount in the left tank goes into left aux,
1761 the amount of the right tank goes into right aux and the
1762 amount of the centre tank goes into the left and right tanks
1763 evenly distributed."""
1764 (rawFuelData, totalFuel) = \
1765 super(DC3Model, self)._convertFuelData(data, index, addCapacities)
1766
1767 centreAmount = rawFuelData[1][1]
1768 if addCapacities:
1769 centreCapacity = rawFuelData[1][2]
1770 self._leftLevel = self._rightLevel = \
1771 centreAmount / centreCapacity / 2.0
1772 fuelData = [(const.FUELTANK_LEFT_AUX,
1773 rawFuelData[0][1], rawFuelData[0][2]),
1774 (const.FUELTANK_LEFT,
1775 centreAmount/2.0, centreCapacity/2.0),
1776 (const.FUELTANK_RIGHT,
1777 centreAmount/2.0, centreCapacity/2.0),
1778 (const.FUELTANK_RIGHT_AUX,
1779 rawFuelData[2][1], rawFuelData[2][2])]
1780 else:
1781 fuelData = [(const.FUELTANK_LEFT_AUX, rawFuelData[0][1]),
1782 (const.FUELTANK_LEFT, centreAmount/2.0),
1783 (const.FUELTANK_RIGHT, centreAmount/2.0),
1784 (const.FUELTANK_RIGHT_AUX, rawFuelData[2][1])]
1785
1786 return (fuelData, totalFuel)
1787
1788 def setFuelLevel(self, handler, levels):
1789 """Set the fuel level.
1790
1791 See the description of Simulator.setFuelLevel. This
1792 implementation assumes to get the four-tank representation,
1793 as returned by getFuel()."""
1794 leftLevel = None
1795 centreLevel = None
1796 rightLevel = None
1797
1798 for (tank, level) in levels:
1799 if tank==const.FUELTANK_LEFT_AUX:
1800 leftLevel = level if leftLevel is None else (leftLevel + level)
1801 elif tank==const.FUELTANK_LEFT:
1802 level /= 2.0
1803 centreLevel = (self._rightLevel + level) \
1804 if centreLevel is None else (centreLevel + level)
1805 self._leftLevel = level
1806 elif tank==const.FUELTANK_RIGHT:
1807 level /= 2.0
1808 centreLevel = (self._leftLevel + level) \
1809 if centreLevel is None else (centreLevel + level)
1810 self._rightLevel = level
1811 elif tank==const.FUELTANK_RIGHT_AUX:
1812 rightLevel = level if rightLevel is None \
1813 else (rightLevel + level)
1814
1815 levels = []
1816 if leftLevel is not None: levels.append((const.FUELTANK_LEFT,
1817 leftLevel))
1818 if centreLevel is not None: levels.append((const.FUELTANK_CENTRE,
1819 centreLevel))
1820 if rightLevel is not None: levels.append((const.FUELTANK_RIGHT,
1821 rightLevel))
1822
1823 super(DC3Model, self).setFuelLevel(handler, levels)
1824
1825#------------------------------------------------------------------------------
1826
1827class T134Model(GenericAircraftModel):
1828 """Generic model for the Tupolev Tu-134 aircraft."""
1829 fuelTanks = [const.FUELTANK_LEFT_TIP, const.FUELTANK_EXTERNAL1,
1830 const.FUELTANK_LEFT_AUX,
1831 const.FUELTANK_CENTRE,
1832 const.FUELTANK_RIGHT_AUX,
1833 const.FUELTANK_EXTERNAL2, const.FUELTANK_RIGHT_TIP]
1834
1835 def __init__(self):
1836 """Construct the model."""
1837 super(T134Model, self). \
1838 __init__(flapsNotches = [0, 10, 20, 30],
1839 fuelTanks = T134Model.fuelTanks,
1840 numEngines = 2)
1841
1842 @property
1843 def name(self):
1844 """Get the name for this aircraft model."""
1845 return "FSUIPC/Generic Tupolev Tu-134"
1846
1847#------------------------------------------------------------------------------
1848
1849class T154Model(GenericAircraftModel):
1850 """Generic model for the Tupolev Tu-134 aircraft."""
1851 fuelTanks = [const.FUELTANK_LEFT_AUX, const.FUELTANK_LEFT,
1852 const.FUELTANK_CENTRE, const.FUELTANK_CENTRE2,
1853 const.FUELTANK_RIGHT, const.FUELTANK_RIGHT_AUX]
1854
1855 def __init__(self):
1856 """Construct the model."""
1857 super(T154Model, self). \
1858 __init__(flapsNotches = [0, 15, 28, 45],
1859 fuelTanks = T154Model.fuelTanks,
1860 numEngines = 3)
1861
1862 @property
1863 def name(self):
1864 """Get the name for this aircraft model."""
1865 return "FSUIPC/Generic Tupolev Tu-154"
1866
1867 def getAircraftState(self, aircraft, timestamp, data):
1868 """Get an aircraft state object for the given monitoring data.
1869
1870 This removes the reverser value for the middle engine."""
1871 state = super(T154Model, self).getAircraftState(aircraft, timestamp, data)
1872 del state.reverser[1]
1873 return state
1874
1875#------------------------------------------------------------------------------
1876
1877class YK40Model(GenericAircraftModel):
1878 """Generic model for the Yakovlev Yak-40 aircraft."""
1879 fuelTanks = [const.FUELTANK_LEFT, const.FUELTANK_RIGHT]
1880
1881 def __init__(self):
1882 """Construct the model."""
1883 super(YK40Model, self). \
1884 __init__(flapsNotches = [0, 20, 35],
1885 fuelTanks = YK40Model.fuelTanks,
1886 numEngines = 2)
1887
1888 @property
1889 def name(self):
1890 """Get the name for this aircraft model."""
1891 return "FSUIPC/Generic Yakovlev Yak-40"
1892
1893#------------------------------------------------------------------------------
1894
1895_genericModels = { const.AIRCRAFT_B736 : B737Model,
1896 const.AIRCRAFT_B737 : B737Model,
1897 const.AIRCRAFT_B738 : B737Model,
1898 const.AIRCRAFT_B738C : B737Model,
1899 const.AIRCRAFT_B733 : B737Model,
1900 const.AIRCRAFT_B734 : B737Model,
1901 const.AIRCRAFT_B735 : B737Model,
1902 const.AIRCRAFT_DH8D : DH8DModel,
1903 const.AIRCRAFT_B762 : B767Model,
1904 const.AIRCRAFT_B763 : B767Model,
1905 const.AIRCRAFT_CRJ2 : B767Model,
1906 const.AIRCRAFT_F70 : F70Model,
1907 const.AIRCRAFT_DC3 : DC3Model,
1908 const.AIRCRAFT_T134 : T134Model,
1909 const.AIRCRAFT_T154 : T154Model,
1910 const.AIRCRAFT_YK40 : YK40Model }
1911
1912#------------------------------------------------------------------------------
1913
1914AircraftModel.registerSpecial(PMDGBoeing737NGModel)
1915AircraftModel.registerSpecial(DreamwingsDH8DModel)
1916AircraftModel.registerSpecial(DAF70Model)
1917
1918#------------------------------------------------------------------------------
Note: See TracBrowser for help on using the repository browser.