source: src/mlx/fsuipc.py@ 59:a3e0b8455dc8

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

Implemented better connection and connection failure handling.

File size: 48.7 KB
Line 
1# Module handling the connection to FSUIPC
2
3#------------------------------------------------------------------------------
4
5import fs
6import const
7import util
8
9import threading
10import os
11import time
12import calendar
13import sys
14import codecs
15
16if os.name == "nt":
17 import pyuipc
18else:
19 import pyuipc_sim as pyuipc
20
21#------------------------------------------------------------------------------
22
23class Handler(threading.Thread):
24 """The thread to handle the FSUIPC requests."""
25 @staticmethod
26 def fsuipc2VS(data):
27 """Convert the given vertical speed data read from FSUIPC into feet/min."""
28 return data*60.0/const.FEETTOMETRES/256.0
29
30 @staticmethod
31 def fsuipc2radioAltitude(data):
32 """Convert the given radio altitude data read from FSUIPC into feet."""
33 return data/const.FEETTOMETRES/65536.0
34
35 @staticmethod
36 def fsuipc2Degrees(data):
37 """Convert the given data into degrees."""
38 return data * 360.0 / 65536.0 / 65536.0
39
40 @staticmethod
41 def fsuipc2PositiveDegrees(data):
42 """Convert the given data into positive degrees."""
43 degrees = Handler.fsuipc2Degrees(data)
44 if degrees<0.0: degrees += 360.0
45 return degrees
46
47 @staticmethod
48 def fsuipc2IAS(data):
49 """Convert the given data into indicated airspeed."""
50 return data / 128.0
51
52 @staticmethod
53 def _callSafe(fun):
54 """Call the given function and swallow any exceptions."""
55 try:
56 return fun()
57 except Exception, e:
58 print >> sys.stderr, str(e)
59 return None
60
61 # The number of times a read is attempted
62 NUM_READATTEMPTS = 3
63
64 # The number of connection attempts
65 NUM_CONNECTATTEMPTS = 3
66
67 # The interval between successive connect attempts
68 CONNECT_INTERVAL = 0.25
69
70 @staticmethod
71 def _performRead(data, callback, extra, validator):
72 """Perform a read request.
73
74 If there is a validator, that will be called with the return values,
75 and if the values are wrong, the request is retried at most a certain
76 number of times.
77
78 Return True if the request has succeeded, False if validation has
79 failed during all attempts. An exception may also be thrown if there is
80 some lower-level communication problem."""
81 attemptsLeft = Handler.NUM_READATTEMPTS
82 while attemptsLeft>0:
83 values = pyuipc.read(data)
84 if validator is None or \
85 Handler._callSafe(lambda: validator(values, extra)):
86 Handler._callSafe(lambda: callback(values, extra))
87 return True
88 else:
89 attemptsLeft -= 1
90 return False
91
92 class Request(object):
93 """A simple, one-shot request."""
94 def __init__(self, forWrite, data, callback, extra, validator = None):
95 """Construct the request."""
96 self._forWrite = forWrite
97 self._data = data
98 self._callback = callback
99 self._extra = extra
100 self._validator = validator
101
102 def process(self, time):
103 """Process the request.
104
105 Return True if the request has succeeded, False if data validation
106 has failed for a reading request. An exception may also be thrown
107 if there is some lower-level communication problem."""
108 if self._forWrite:
109 pyuipc.write(self._data)
110 Handler._callSafe(lambda: self._callback(True, self._extra))
111 return True
112 else:
113 return Handler._performRead(self._data, self._callback,
114 self._extra, self._validator)
115
116 def fail(self):
117 """Handle the failure of this request."""
118 if self._forWrite:
119 Handler._callSafe(lambda: self._callback(False, self._extra))
120 else:
121 Handler._callSafe(lambda: self._callback(None, self._extra))
122
123 class PeriodicRequest(object):
124 """A periodic request."""
125 def __init__(self, id, period, data, callback, extra, validator):
126 """Construct the periodic request."""
127 self._id = id
128 self._period = period
129 self._nextFire = time.time() + period
130 self._data = data
131 self._preparedData = None
132 self._callback = callback
133 self._extra = extra
134 self._validator = validator
135
136 @property
137 def id(self):
138 """Get the ID of this periodic request."""
139 return self._id
140
141 @property
142 def nextFire(self):
143 """Get the next firing time."""
144 return self._nextFire
145
146 def process(self, time):
147 """Check if this request should be executed, and if so, do so.
148
149 time is the time at which the request is being executed. If this
150 function is called too early, nothing is done, and True is
151 returned.
152
153 Return True if the request has succeeded, False if data validation
154 has failed. An exception may also be thrown if there is some
155 lower-level communication problem."""
156 if time<self._nextFire:
157 return True
158
159 if self._preparedData is None:
160 self._preparedData = pyuipc.prepare_data(self._data)
161 self._data = None
162
163 isOK = Handler._performRead(self._preparedData, self._callback,
164 self._extra, self._validator)
165
166 if isOK:
167 while self._nextFire <= time:
168 self._nextFire += self._period
169
170 return isOK
171
172 def fail(self):
173 """Handle the failure of this request."""
174 pass
175
176 def __cmp__(self, other):
177 """Compare two periodic requests. They are ordered by their next
178 firing times."""
179 return cmp(self._nextFire, other._nextFire)
180
181 def __init__(self, connectionListener,
182 connectAttempts = -1, connectInterval = 0.2):
183 """Construct the handler with the given connection listener."""
184 threading.Thread.__init__(self)
185
186 self._connectionListener = connectionListener
187 self._connectAttempts = connectAttempts
188 self._connectInterval = connectInterval
189
190 self._requestCondition = threading.Condition()
191 self._connectionRequested = False
192 self._connected = False
193
194 self._requests = []
195 self._nextPeriodicID = 1
196 self._periodicRequests = []
197
198 self.daemon = True
199
200 def requestRead(self, data, callback, extra = None, validator = None):
201 """Request the reading of some data.
202
203 data is a list of tuples of the following items:
204 - the offset of the data as an integer
205 - the type letter of the data as a string
206
207 callback is a function that receives two pieces of data:
208 - the values retrieved or None on error
209 - the extra parameter
210
211 It will be called in the handler's thread!
212 """
213 with self._requestCondition:
214 self._requests.append(Handler.Request(False, data, callback, extra,
215 validator))
216 self._requestCondition.notify()
217
218 def requestWrite(self, data, callback, extra = None):
219 """Request the writing of some data.
220
221 data is a list of tuples of the following items:
222 - the offset of the data as an integer
223 - the type letter of the data as a string
224 - the data to write
225
226 callback is a function that receives two pieces of data:
227 - a boolean indicating if writing was successful
228 - the extra data
229 It will be called in the handler's thread!
230 """
231 with self._requestCondition:
232 self._requests.append(Handler.Request(True, data, callback, extra))
233 self._requestCondition.notify()
234
235 @staticmethod
236 def _readWriteCallback(data, extra):
237 """Callback for the read() and write() calls below."""
238 extra.append(data)
239 with extra[0] as condition:
240 condition.notify()
241
242 def requestPeriodicRead(self, period, data, callback, extra = None,
243 validator = None):
244 """Request a periodic read of data.
245
246 period is a floating point number with the period in seconds.
247
248 This function returns an identifier which can be used to cancel the
249 request."""
250 with self._requestCondition:
251 id = self._nextPeriodicID
252 self._nextPeriodicID += 1
253 request = Handler.PeriodicRequest(id, period, data, callback,
254 extra, validator)
255 self._periodicRequests.append(request)
256 self._requestCondition.notify()
257 return id
258
259 def clearPeriodic(self, id):
260 """Clear the periodic request with the given ID."""
261 with self._requestCondition:
262 for i in range(0, len(self._periodicRequests)):
263 if self._periodicRequests[i].id==id:
264 del self._periodicRequests[i]
265 return True
266 return False
267
268 def connect(self):
269 """Initiate the connection to the flight simulator."""
270 with self._requestCondition:
271 if not self._connectionRequested:
272 self._connectionRequested = True
273 self._requestCondition.notify()
274
275 def disconnect(self):
276 """Disconnect from the flight simulator."""
277 with self._requestCondition:
278 self._requests = []
279 if self._connectionRequested:
280 self._connectionRequested = False
281 self._requestCondition.notify()
282
283 def clearRequests(self):
284 """Clear the outstanding one-shot requests."""
285 with self._requestCondition:
286 self._requests = []
287
288 def run(self):
289 """Perform the operation of the thread."""
290 while True:
291 self._waitConnectionRequest()
292
293 if self._connect():
294 self._handleConnection()
295
296 self._disconnect()
297
298 def _waitConnectionRequest(self):
299 """Wait for a connection request to arrive."""
300 with self._requestCondition:
301 while not self._connectionRequested:
302 self._requestCondition.wait()
303
304 def _connect(self, autoReconnection = False):
305 """Try to connect to the flight simulator via FSUIPC
306
307 Returns True if the connection has been established, False if it was
308 not due to no longer requested.
309 """
310 attempts = 0
311 while self._connectionRequested:
312 try:
313 attempts += 1
314 pyuipc.open(pyuipc.SIM_ANY)
315 description = "(FSUIPC version: 0x%04x, library version: 0x%04x, FS version: %d)" % \
316 (pyuipc.fsuipc_version, pyuipc.lib_version,
317 pyuipc.fs_version)
318 if not autoReconnection:
319 Handler._callSafe(lambda:
320 self._connectionListener.connected(const.SIM_MSFS9,
321 description))
322 self._connected = True
323 return True
324 except Exception, e:
325 print "fsuipc.Handler._connect: connection failed: " + str(e)
326 if attempts<self.NUM_CONNECTATTEMPTS:
327 time.sleep(self.CONNECT_INTERVAL)
328 else:
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
337 return False
338
339 def _handleConnection(self):
340 """Handle a living connection."""
341 with self._requestCondition:
342 while self._connectionRequested:
343 self._processRequests()
344 self._waitRequest()
345
346 def _waitRequest(self):
347 """Wait for the time of the next request.
348
349 Returns also, if the connection is no longer requested.
350
351 Should be called with the request condition lock held."""
352 while self._connectionRequested:
353 timeout = None
354 if self._periodicRequests:
355 self._periodicRequests.sort()
356 timeout = self._periodicRequests[0].nextFire - time.time()
357
358 if timeout is not None and timeout <= 0.0:
359 return
360
361 self._requestCondition.wait(timeout)
362
363 def _disconnect(self):
364 """Disconnect from the flight simulator."""
365 if self._connected:
366 pyuipc.close()
367 self._connected = False
368
369 def _processRequest(self, request, time):
370 """Process the given request.
371
372 If an exception occurs or invalid data is read too many times, we try
373 to reconnect.
374
375 This function returns only if the request has succeeded, or if a
376 connection is no longer requested.
377
378 This function is called with the request lock held, but is relased
379 whole processing the request and reconnecting."""
380 self._requestCondition.release()
381
382 needReconnect = False
383 try:
384 try:
385 if not request.process(time):
386 print "fsuipc.Handler._processRequest: FSUIPC returned invalid data too many times, reconnecting"
387 needReconnect = True
388 except Exception as e:
389 print "fsuipc.Handler._processRequest: FSUIPC connection failed (" + \
390 str(e) + "), reconnecting."
391 needReconnect = True
392
393 if needReconnect:
394 with self._requestCondition:
395 self._requests.insert(0, request)
396 self._disconnect()
397 self._connect(autoReconnection = True)
398 finally:
399 self._requestCondition.acquire()
400
401 def _processRequests(self):
402 """Process any pending requests.
403
404 Will be called with the request lock held."""
405 while self._connectionRequested and self._periodicRequests:
406 self._periodicRequests.sort()
407 request = self._periodicRequests[0]
408
409 t = time.time()
410
411 if request.nextFire>t:
412 break
413
414 self._processRequest(request, t)
415
416 while self._connectionRequested and self._requests:
417 request = self._requests[0]
418 del self._requests[0]
419
420 self._processRequest(request, None)
421
422 return self._connectionRequested
423
424#------------------------------------------------------------------------------
425
426class Simulator(object):
427 """The simulator class representing the interface to the flight simulator
428 via FSUIPC."""
429 # The basic data that should be queried all the time once we are connected
430 normalData = [ (0x0240, "H"), # Year
431 (0x023e, "H"), # Number of day in year
432 (0x023b, "b"), # UTC hour
433 (0x023c, "b"), # UTC minute
434 (0x023a, "b"), # seconds
435 (0x3d00, -256), # The name of the current aircraft
436 (0x3c00, -256) ] # The path of the current AIR file
437
438 flareData1 = [ (0x023a, "b"), # Seconds of time
439 (0x31e4, "d"), # Radio altitude
440 (0x02c8, "d") ] # Vertical speed
441
442 flareStartData = [ (0x0e90, "H"), # Ambient wind speed
443 (0x0e92, "H"), # Ambient wind direction
444 (0x0e8a, "H") ] # Visibility
445
446 flareData2 = [ (0x023a, "b"), # Seconds of time
447 (0x0366, "H"), # On the ground
448 (0x02c8, "d"), # Vertical speed
449 (0x030c, "d"), # Touch-down rate
450 (0x02bc, "d"), # IAS
451 (0x0578, "d"), # Pitch
452 (0x057c, "d"), # Bank
453 (0x0580, "d") ] # Heading
454
455 def __init__(self, connectionListener, connectAttempts = -1,
456 connectInterval = 0.2):
457 """Construct the simulator.
458
459 The aircraft object passed must provide the following members:
460 - type: one of the AIRCRAFT_XXX constants from const.py
461 - modelChanged(aircraftName, modelName): called when the model handling
462 the aircraft has changed.
463 - handleState(aircraftState): handle the given state.
464 - flareStarted(windSpeed, windDirection, visibility, flareStart,
465 flareStartFS): called when the flare has
466 started. windSpeed is in knots, windDirection is in degrees and
467 visibility is in metres. flareStart and flareStartFS are two time
468 values expressed in seconds that can be used to calculate the flare
469 time.
470 - flareFinished(flareEnd, flareEndFS, tdRate, tdRateCalculatedByFS,
471 ias, pitch, bank, heading): called when the flare has
472 finished, i.e. the aircraft is on the ground. flareEnd and flareEndFS
473 are the two time values corresponding to the touchdown time. tdRate is
474 the touch-down rate, tdRateCalculatedBySim indicates if the data comes
475 from the simulator or was calculated by the adapter. The other data
476 are self-explanatory and expressed in their 'natural' units."""
477 self._aircraft = None
478
479 self._handler = Handler(connectionListener,
480 connectAttempts = connectAttempts,
481 connectInterval = connectInterval)
482 self._handler.start()
483
484 self._normalRequestID = None
485
486 self._monitoringRequested = False
487 self._monitoring = False
488
489 self._aircraftName = None
490 self._aircraftModel = None
491
492 self._flareRequestID = None
493 self._flareRates = []
494 self._flareStart = None
495 self._flareStartFS = None
496
497 self._latin1decoder = codecs.getdecoder("iso-8859-1")
498
499 def connect(self, aircraft):
500 """Initiate a connection to the simulator."""
501 self._aircraft = aircraft
502 self._aircraftName = None
503 self._aircraftModel = None
504 self._handler.connect()
505 if self._normalRequestID is None:
506 self._startDefaultNormal()
507
508 def reconnect(self):
509 """Initiate a reconnection to the simulator.
510
511 It does not reset already set up data, just calls connect() on the
512 handler."""
513 self._handler.connect()
514
515 def requestZFW(self, callback):
516 """Send a request for the ZFW."""
517 self._handler.requestRead([(0x3bfc, "d")], self._handleZFW, extra = callback)
518
519 def startMonitoring(self):
520 """Start the periodic monitoring of the aircraft and pass the resulting
521 state to the aircraft object periodically."""
522 assert not self._monitoringRequested
523 self._monitoringRequested = True
524
525 def stopMonitoring(self):
526 """Stop the periodic monitoring of the aircraft."""
527 assert self._monitoringRequested
528 self._monitoringRequested = False
529
530 def startFlare(self):
531 """Start monitoring the flare time.
532
533 At present it is assumed to be called from the FSUIPC thread, hence no
534 protection."""
535 #self._aircraft.logger.debug("startFlare")
536 if self._flareRequestID is None:
537 self._flareRates = []
538 self._flareRequestID = self._handler.requestPeriodicRead(0.1,
539 Simulator.flareData1,
540 self._handleFlare1)
541
542 def cancelFlare(self):
543 """Cancel monitoring the flare time.
544
545 At present it is assumed to be called from the FSUIPC thread, hence no
546 protection."""
547 if self._flareRequestID is not None:
548 self._handler.clearPeriodic(self._flareRequestID)
549 self._flareRequestID = None
550
551 def disconnect(self):
552 """Disconnect from the simulator."""
553 assert not self._monitoringRequested
554
555 self._stopNormal()
556 self._handler.disconnect()
557
558 def _startDefaultNormal(self):
559 """Start the default normal periodic request."""
560 assert self._normalRequestID is None
561 self._normalRequestID = \
562 self._handler.requestPeriodicRead(1.0,
563 Simulator.normalData,
564 self._handleNormal,
565 validator = self._validateNormal)
566
567 def _stopNormal(self):
568 """Stop the normal period request."""
569 assert self._normalRequestID is not None
570 self._handler.clearPeriodic(self._normalRequestID)
571 self._normalRequestID = None
572 self._monitoring = False
573
574 def _validateNormal(self, data, extra):
575 """Validate the normal data."""
576 return data[0]!=0 and data[1]!=0 and len(data[5])>0 and len(data[6])>0
577
578 def _handleNormal(self, data, extra):
579 """Handle the reply to the normal request.
580
581 At the beginning the result consists the data for normalData. When
582 monitoring is started, it contains the result also for the
583 aircraft-specific values.
584 """
585 timestamp = calendar.timegm(time.struct_time([data[0],
586 1, 1, 0, 0, 0, -1, 1, 0]))
587 timestamp += data[1] * 24 * 3600
588 timestamp += data[2] * 3600
589 timestamp += data[3] * 60
590 timestamp += data[4]
591
592 createdNewModel = self._setAircraftName(timestamp, data[5], data[6])
593
594 if self._monitoringRequested and not self._monitoring:
595 self._stopNormal()
596 self._startMonitoring()
597 elif self._monitoring and not self._monitoringRequested:
598 self._stopNormal()
599 self._startDefaultNormal()
600 elif self._monitoring and self._aircraftModel is not None and \
601 not createdNewModel:
602 aircraftState = self._aircraftModel.getAircraftState(self._aircraft,
603 timestamp, data)
604 self._aircraft.handleState(aircraftState)
605
606 def _setAircraftName(self, timestamp, name, airPath):
607 """Set the name of the aicraft and if it is different from the
608 previous, create a new model for it.
609
610 If so, also notifty the aircraft about the change.
611
612 Return if a new model was created."""
613 aircraftName = (name, airPath)
614 if aircraftName==self._aircraftName:
615 return False
616
617 self._aircraftName = aircraftName
618 needNew = self._aircraftModel is None
619 needNew = needNew or\
620 not self._aircraftModel.doesHandle(self._aircraft, aircraftName)
621 if not needNew:
622 specialModel = AircraftModel.findSpecial(self._aircraft, aircraftName)
623 needNew = specialModel is not None and \
624 specialModel is not self._aircraftModel.__class__
625
626 if needNew:
627 self._setAircraftModel(AircraftModel.create(self._aircraft, aircraftName))
628
629
630 self._aircraft.modelChanged(timestamp, self._latin1decoder(name),
631 self._aircraftModel.name)
632
633 return needNew
634
635 def _setAircraftModel(self, model):
636 """Set a new aircraft model.
637
638 It will be queried for the data to monitor and the monitoring request
639 will be replaced by a new one."""
640 self._aircraftModel = model
641
642 if self._monitoring:
643 self._stopNormal()
644 self._startMonitoring()
645
646 def _startMonitoring(self):
647 """Start monitoring with the current aircraft model."""
648 data = Simulator.normalData[:]
649 self._aircraftModel.addMonitoringData(data)
650
651 self._normalRequestID = \
652 self._handler.requestPeriodicRead(1.0, data,
653 self._handleNormal,
654 validator = self._validateNormal)
655 self._monitoring = True
656
657 def _addFlareRate(self, data):
658 """Append a flare rate to the list of last rates."""
659 if len(self._flareRates)>=3:
660 del self._flareRates[0]
661 self._flareRates.append(Handler.fsuipc2VS(data))
662
663 def _handleFlare1(self, data, normal):
664 """Handle the first stage of flare monitoring."""
665 #self._aircraft.logger.debug("handleFlare1: " + str(data))
666 if Handler.fsuipc2radioAltitude(data[1])<=50.0:
667 self._flareStart = time.time()
668 self._flareStartFS = data[0]
669 self._handler.clearPeriodic(self._flareRequestID)
670 self._flareRequestID = \
671 self._handler.requestPeriodicRead(0.1,
672 Simulator.flareData2,
673 self._handleFlare2)
674 self._handler.requestRead(Simulator.flareStartData,
675 self._handleFlareStart)
676
677 self._addFlareRate(data[2])
678
679 def _handleFlareStart(self, data, extra):
680 """Handle the data need to notify the aircraft about the starting of
681 the flare."""
682 #self._aircraft.logger.debug("handleFlareStart: " + str(data))
683 if data is not None:
684 windDirection = data[1]*360.0/65536.0
685 if windDirection<0.0: windDirection += 360.0
686 self._aircraft.flareStarted(data[0], windDirection,
687 data[2]*1609.344/100.0,
688 self._flareStart, self._flareStartFS)
689
690 def _handleFlare2(self, data, normal):
691 """Handle the first stage of flare monitoring."""
692 #self._aircraft.logger.debug("handleFlare2: " + str(data))
693 if data[1]!=0:
694 flareEnd = time.time()
695 self._handler.clearPeriodic(self._flareRequestID)
696 self._flareRequestID = None
697
698 flareEndFS = data[0]
699 if flareEndFS<self._flareStartFS:
700 flareEndFS += 60
701
702 tdRate = Handler.fsuipc2VS(data[3])
703 tdRateCalculatedByFS = True
704 if tdRate==0 or tdRate>1000.0 or tdRate<-1000.0:
705 tdRate = min(self._flareRates)
706 tdRateCalculatedByFS = False
707
708 self._aircraft.flareFinished(flareEnd, flareEndFS,
709 tdRate, tdRateCalculatedByFS,
710 Handler.fsuipc2IAS(data[4]),
711 Handler.fsuipc2Degrees(data[5]),
712 Handler.fsuipc2Degrees(data[6]),
713 Handler.fsuipc2PositiveDegrees(data[7]))
714 else:
715 self._addFlareRate(data[2])
716
717 def _handleZFW(self, data, callback):
718 """Callback for a ZFW retrieval request."""
719 zfw = data[0] * const.LBSTOKG / 256.0
720 callback(zfw)
721
722#------------------------------------------------------------------------------
723
724class AircraftModel(object):
725 """Base class for the aircraft models.
726
727 Aircraft models handle the data arriving from FSUIPC and turn it into an
728 object describing the aircraft's state."""
729 monitoringData = [("paused", 0x0264, "H"),
730 ("frozen", 0x3364, "H"),
731 ("replay", 0x0628, "d"),
732 ("slew", 0x05dc, "H"),
733 ("overspeed", 0x036d, "b"),
734 ("stalled", 0x036c, "b"),
735 ("onTheGround", 0x0366, "H"),
736 ("zfw", 0x3bfc, "d"),
737 ("grossWeight", 0x30c0, "f"),
738 ("heading", 0x0580, "d"),
739 ("pitch", 0x0578, "d"),
740 ("bank", 0x057c, "d"),
741 ("ias", 0x02bc, "d"),
742 ("mach", 0x11c6, "H"),
743 ("groundSpeed", 0x02b4, "d"),
744 ("vs", 0x02c8, "d"),
745 ("radioAltitude", 0x31e4, "d"),
746 ("altitude", 0x0570, "l"),
747 ("gLoad", 0x11ba, "H"),
748 ("flapsControl", 0x0bdc, "d"),
749 ("flapsLeft", 0x0be0, "d"),
750 ("flapsRight", 0x0be4, "d"),
751 ("lights", 0x0d0c, "H"),
752 ("pitot", 0x029c, "b"),
753 ("parking", 0x0bc8, "H"),
754 ("noseGear", 0x0bec, "d"),
755 ("spoilersArmed", 0x0bcc, "d"),
756 ("spoilers", 0x0bd0, "d"),
757 ("altimeter", 0x0330, "H"),
758 ("nav1", 0x0350, "H"),
759 ("nav2", 0x0352, "H"),
760 ("squawk", 0x0354, "H"),
761 ("windSpeed", 0x0e90, "H"),
762 ("windDirection", 0x0e92, "H")]
763
764
765 specialModels = []
766
767 @staticmethod
768 def registerSpecial(clazz):
769 """Register the given class as a special model."""
770 AircraftModel.specialModels.append(clazz)
771
772 @staticmethod
773 def findSpecial(aircraft, aircraftName):
774 for specialModel in AircraftModel.specialModels:
775 if specialModel.doesHandle(aircraft, aircraftName):
776 return specialModel
777 return None
778
779 @staticmethod
780 def create(aircraft, aircraftName):
781 """Create the model for the given aircraft name, and notify the
782 aircraft about it."""
783 specialModel = AircraftModel.findSpecial(aircraft, aircraftName)
784 if specialModel is not None:
785 return specialModel()
786 if aircraft.type in _genericModels:
787 return _genericModels[aircraft.type]()
788 else:
789 return GenericModel()
790
791 @staticmethod
792 def convertBCD(data, length):
793 """Convert a data item encoded as BCD into a string of the given number
794 of digits."""
795 bcd = ""
796 for i in range(0, length):
797 digit = chr(ord('0') + (data&0x0f))
798 data >>= 4
799 bcd = digit + bcd
800 return bcd
801
802 @staticmethod
803 def convertFrequency(data):
804 """Convert the given frequency data to a string."""
805 bcd = AircraftModel.convertBCD(data, 4)
806 return "1" + bcd[0:2] + "." + bcd[2:4]
807
808 def __init__(self, flapsNotches):
809 """Construct the aircraft model.
810
811 flapsNotches is a list of degrees of flaps that are available on the aircraft."""
812 self._flapsNotches = flapsNotches
813
814 @property
815 def name(self):
816 """Get the name for this aircraft model."""
817 return "FSUIPC/Generic"
818
819 def doesHandle(self, aircraft, aircraftName):
820 """Determine if the model handles the given aircraft name.
821
822 This default implementation returns False."""
823 return False
824
825 def _addOffsetWithIndexMember(self, dest, offset, type, attrName = None):
826 """Add the given FSUIPC offset and type to the given array and a member
827 attribute with the given name."""
828 dest.append((offset, type))
829 if attrName is not None:
830 setattr(self, attrName, len(dest)-1)
831
832 def _addDataWithIndexMembers(self, dest, prefix, data):
833 """Add FSUIPC data to the given array and also corresponding index
834 member variables with the given prefix.
835
836 data is a list of triplets of the following items:
837 - the name of the data item. The index member variable will have a name
838 created by prepending the given prefix to this name.
839 - the FSUIPC offset
840 - the FSUIPC type
841
842 The latter two items will be appended to dest."""
843 for (name, offset, type) in data:
844 self._addOffsetWithIndexMember(dest, offset, type, prefix + name)
845
846 def addMonitoringData(self, data):
847 """Add the model-specific monitoring data to the given array."""
848 self._addDataWithIndexMembers(data, "_monidx_",
849 AircraftModel.monitoringData)
850
851 def getAircraftState(self, aircraft, timestamp, data):
852 """Get an aircraft state object for the given monitoring data."""
853 state = fs.AircraftState()
854
855 state.timestamp = timestamp
856
857 state.paused = data[self._monidx_paused]!=0 or \
858 data[self._monidx_frozen]!=0 or \
859 data[self._monidx_replay]!=0
860 state.trickMode = data[self._monidx_slew]!=0
861
862 state.overspeed = data[self._monidx_overspeed]!=0
863 state.stalled = data[self._monidx_stalled]!=0
864 state.onTheGround = data[self._monidx_onTheGround]!=0
865
866 state.zfw = data[self._monidx_zfw] * const.LBSTOKG / 256.0
867 state.grossWeight = data[self._monidx_grossWeight] * const.LBSTOKG
868
869 state.heading = Handler.fsuipc2PositiveDegrees(data[self._monidx_heading])
870
871 state.pitch = Handler.fsuipc2Degrees(data[self._monidx_pitch])
872 state.bank = Handler.fsuipc2Degrees(data[self._monidx_bank])
873
874 state.ias = Handler.fsuipc2IAS(data[self._monidx_ias])
875 state.mach = data[self._monidx_mach] / 20480.0
876 state.groundSpeed = data[self._monidx_groundSpeed]* 3600.0/65536.0/1852.0
877 state.vs = Handler.fsuipc2VS(data[self._monidx_vs])
878
879 state.radioAltitude = \
880 Handler.fsuipc2radioAltitude(data[self._monidx_radioAltitude])
881 state.altitude = data[self._monidx_altitude]/const.FEETTOMETRES/65536.0/65536.0
882
883 state.gLoad = data[self._monidx_gLoad] / 625.0
884
885 numNotchesM1 = len(self._flapsNotches) - 1
886 flapsIncrement = 16383 / numNotchesM1
887 flapsControl = data[self._monidx_flapsControl]
888 flapsIndex = flapsControl / flapsIncrement
889 if flapsIndex < numNotchesM1:
890 if (flapsControl - (flapsIndex*flapsIncrement) >
891 (flapsIndex+1)*flapsIncrement - flapsControl):
892 flapsIndex += 1
893 state.flapsSet = self._flapsNotches[flapsIndex]
894
895 flapsLeft = data[self._monidx_flapsLeft]
896 state.flaps = self._flapsNotches[-1]*flapsLeft/16383.0
897
898 lights = data[self._monidx_lights]
899
900 state.navLightsOn = (lights&0x01) != 0
901 state.antiCollisionLightsOn = (lights&0x02) != 0
902 state.landingLightsOn = (lights&0x04) != 0
903 state.strobeLightsOn = (lights&0x10) != 0
904
905 state.pitotHeatOn = data[self._monidx_pitot]!=0
906
907 state.parking = data[self._monidx_parking]!=0
908
909 state.gearsDown = data[self._monidx_noseGear]==16383
910
911 state.spoilersArmed = data[self._monidx_spoilersArmed]!=0
912
913 spoilers = data[self._monidx_spoilers]
914 if spoilers<=4800:
915 state.spoilersExtension = 0.0
916 else:
917 state.spoilersExtension = (spoilers - 4800) * 100.0 / (16383 - 4800)
918
919 state.altimeter = data[self._monidx_altimeter] / 16.0
920
921 state.nav1 = AircraftModel.convertFrequency(data[self._monidx_nav1])
922 state.nav2 = AircraftModel.convertFrequency(data[self._monidx_nav2])
923 state.squawk = AircraftModel.convertBCD(data[self._monidx_squawk], 4)
924
925 state.windSpeed = data[self._monidx_windSpeed]
926 state.windDirection = data[self._monidx_windDirection]*360.0/65536.0
927 if state.windDirection<0.0: state.windDirection += 360.0
928
929 return state
930
931#------------------------------------------------------------------------------
932
933class GenericAircraftModel(AircraftModel):
934 """A generic aircraft model that can handle the fuel levels, the N1 or RPM
935 values and some other common parameters in a generic way."""
936 def __init__(self, flapsNotches, fuelInfo, numEngines, isN1 = True):
937 """Construct the generic aircraft model with the given data.
938
939 flapsNotches is an array of how much degrees the individual flaps
940 notches mean.
941
942 fuelInfo is an array of FSUIPC offsets for the levels of the fuel
943 tanks. It is assumed to be a 4-byte value, followed by another 4-byte
944 value, which is the fuel tank capacity.
945
946 numEngines is the number of engines the aircraft has.
947
948 isN1 determines if the engines have an N1 value or an RPM value
949 (e.g. pistons)."""
950 super(GenericAircraftModel, self).__init__(flapsNotches = flapsNotches)
951
952 self._fuelInfo = fuelInfo
953 self._fuelStartIndex = None
954 self._numEngines = numEngines
955 self._engineStartIndex = None
956 self._isN1 = isN1
957
958 def doesHandle(self, aircraft, aircraftName):
959 """Determine if the model handles the given aircraft name.
960
961 This implementation returns True."""
962 return True
963
964 def addMonitoringData(self, data):
965 """Add the model-specific monitoring data to the given array."""
966 super(GenericAircraftModel, self).addMonitoringData(data)
967
968 self._addOffsetWithIndexMember(data, 0x0af4, "H", "_monidx_fuelWeight")
969
970 self._fuelStartIndex = len(data)
971 for offset in self._fuelInfo:
972 self._addOffsetWithIndexMember(data, offset, "u") # tank level
973 self._addOffsetWithIndexMember(data, offset+4, "u") # tank capacity
974
975 if self._isN1:
976 self._engineStartIndex = len(data)
977 for i in range(0, self._numEngines):
978 self._addOffsetWithIndexMember(data, 0x2000 + i * 0x100, "f") # N1
979 self._addOffsetWithIndexMember(data, 0x088c + i * 0x98, "h") # throttle lever
980
981 def getAircraftState(self, aircraft, timestamp, data):
982 """Get the aircraft state.
983
984 Get it from the parent, and then add the data about the fuel levels and
985 the engine parameters."""
986 state = super(GenericAircraftModel, self).getAircraftState(aircraft,
987 timestamp,
988 data)
989
990 fuelWeight = data[self._monidx_fuelWeight]/256.0
991 state.fuel = []
992 for i in range(self._fuelStartIndex,
993 self._fuelStartIndex + 2*len(self._fuelInfo), 2):
994 fuel = data[i+1]*data[i]*fuelWeight*const.LBSTOKG/128.0/65536.0
995 state.fuel.append(fuel)
996
997 state.n1 = []
998 state.reverser = []
999 for i in range(self._engineStartIndex,
1000 self._engineStartIndex + 2*self._numEngines, 2):
1001 state.n1.append(data[i])
1002 state.reverser.append(data[i+1]<0)
1003
1004 return state
1005
1006#------------------------------------------------------------------------------
1007
1008class GenericModel(GenericAircraftModel):
1009 """Generic aircraft model for an unknown type."""
1010 def __init__(self):
1011 """Construct the model."""
1012 super(GenericModel, self). \
1013 __init__(flapsNotches = [0, 10, 20, 30],
1014 fuelInfo = [0x0b74, 0x0b7c, 0xb94],
1015 numEngines = 2)
1016
1017 @property
1018 def name(self):
1019 """Get the name for this aircraft model."""
1020 return "FSUIPC/Generic"
1021
1022#------------------------------------------------------------------------------
1023
1024class B737Model(GenericAircraftModel):
1025 """Generic model for the Boeing 737 Classing and NG aircraft."""
1026 def __init__(self):
1027 """Construct the model."""
1028 super(B737Model, self). \
1029 __init__(flapsNotches = [0, 1, 2, 5, 10, 15, 25, 30, 40],
1030 fuelInfo = [0x0b74, 0x0b7c, 0xb94],
1031 numEngines = 2)
1032
1033 @property
1034 def name(self):
1035 """Get the name for this aircraft model."""
1036 return "FSUIPC/Generic Boeing 737"
1037
1038#------------------------------------------------------------------------------
1039
1040class PMDGBoeing737NGModel(B737Model):
1041 """A model handler for the PMDG Boeing 737NG model."""
1042 @staticmethod
1043 def doesHandle(aircraft, (name, airPath)):
1044 """Determine if this model handler handles the aircraft with the given
1045 name."""
1046 return aircraft.type in [const.AIRCRAFT_B736,
1047 const.AIRCRAFT_B737,
1048 const.AIRCRAFT_B738] and \
1049 (name.find("PMDG")!=-1 or airPath.find("PMDG")!=-1) and \
1050 (name.find("737")!=-1 or airPath.find("737")!=-1) and \
1051 (name.find("600")!=-1 or airPath.find("600")!=-1 or \
1052 name.find("700")!=-1 or airPath.find("700")!=-1 or \
1053 name.find("800")!=-1 or airPath.find("800")!=-1 or \
1054 name.find("900")!=-1 or airPath.find("900")!=-1)
1055
1056 @property
1057 def name(self):
1058 """Get the name for this aircraft model."""
1059 return "FSUIPC/PMDG Boeing 737NG"
1060
1061 def addMonitoringData(self, data):
1062 """Add the model-specific monitoring data to the given array."""
1063 super(PMDGBoeing737NGModel, self).addMonitoringData(data)
1064
1065 self._addOffsetWithIndexMember(data, 0x6202, "b", "_pmdgidx_switches")
1066
1067 def getAircraftState(self, aircraft, timestamp, data):
1068 """Get the aircraft state.
1069
1070 Get it from the parent, and then check some PMDG-specific stuff."""
1071 state = super(PMDGBoeing737NGModel, self).getAircraftState(aircraft,
1072 timestamp,
1073 data)
1074 if data[self._pmdgidx_switches]&0x01==0x01:
1075 state.altimeter = 1013.25
1076
1077 return state
1078
1079#------------------------------------------------------------------------------
1080
1081class B767Model(GenericAircraftModel):
1082 """Generic model for the Boeing 767 aircraft."""
1083 def __init__(self):
1084 """Construct the model."""
1085 super(B767Model, self). \
1086 __init__(flapsNotches = [0, 1, 5, 15, 20, 25, 30],
1087 fuelInfo = [0x0b74, 0x0b7c, 0xb94],
1088 numEngines = 2)
1089
1090 @property
1091 def name(self):
1092 """Get the name for this aircraft model."""
1093 return "FSUIPC/Generic Boeing 767"
1094
1095#------------------------------------------------------------------------------
1096
1097class DH8DModel(GenericAircraftModel):
1098 """Generic model for the Bombardier Dash 8-Q400 aircraft."""
1099 def __init__(self):
1100 """Construct the model."""
1101 super(DH8DModel, self). \
1102 __init__(flapsNotches = [0, 5, 10, 15, 35],
1103 fuelInfo = [0x0b74, 0x0b7c, 0xb94],
1104 numEngines = 2)
1105
1106 @property
1107 def name(self):
1108 """Get the name for this aircraft model."""
1109 return "FSUIPC/Generic Bombardier Dash 8-Q400"
1110
1111#------------------------------------------------------------------------------
1112
1113class DreamwingsDH8DModel(DH8DModel):
1114 """Model handler for the Dreamwings Dash 8-Q400."""
1115 @staticmethod
1116 def doesHandle(aircraft, (name, airPath)):
1117 """Determine if this model handler handles the aircraft with the given
1118 name."""
1119 return aircraft.type==const.AIRCRAFT_DH8D and \
1120 (name.find("Dreamwings")!=-1 or airPath.find("Dreamwings")!=-1) and \
1121 (name.find("Dash")!=-1 or airPath.find("Dash")!=-1) and \
1122 (name.find("Q400")!=-1 or airPath.find("Q400")!=-1) and \
1123 airPath.find("Dash8Q400")!=-1
1124
1125 @property
1126 def name(self):
1127 """Get the name for this aircraft model."""
1128 return "FSUIPC/Dreamwings Bombardier Dash 8-Q400"
1129
1130 def getAircraftState(self, aircraft, timestamp, data):
1131 """Get the aircraft state.
1132
1133 Get it from the parent, and then invert the pitot heat state."""
1134 state = super(DreamwingsDH8DModel, self).getAircraftState(aircraft,
1135 timestamp,
1136 data)
1137 state.pitotHeatOn = not state.pitotHeatOn
1138
1139 return state
1140#------------------------------------------------------------------------------
1141
1142class CRJ2Model(GenericAircraftModel):
1143 """Generic model for the Bombardier CRJ-200 aircraft."""
1144 def __init__(self):
1145 """Construct the model."""
1146 super(CRJ2Model, self). \
1147 __init__(flapsNotches = [0, 8, 20, 30, 45],
1148 fuelInfo = [0x0b74, 0x0b7c, 0xb94],
1149 numEngines = 2)
1150
1151 @property
1152 def name(self):
1153 """Get the name for this aircraft model."""
1154 return "FSUIPC/Generic Bombardier CRJ-200"
1155
1156#------------------------------------------------------------------------------
1157
1158class F70Model(GenericAircraftModel):
1159 """Generic model for the Fokker F70 aircraft."""
1160 def __init__(self):
1161 """Construct the model."""
1162 super(F70Model, self). \
1163 __init__(flapsNotches = [0, 8, 15, 25, 42],
1164 fuelInfo = [0x0b74, 0x0b7c, 0xb94],
1165 numEngines = 2)
1166
1167 @property
1168 def name(self):
1169 """Get the name for this aircraft model."""
1170 return "FSUIPC/Generic Fokker 70"
1171
1172#------------------------------------------------------------------------------
1173
1174class DC3Model(GenericAircraftModel):
1175 """Generic model for the Lisunov Li-2 (DC-3) aircraft."""
1176 def __init__(self):
1177 """Construct the model."""
1178 super(DC3Model, self). \
1179 __init__(flapsNotches = [0, 15, 30, 45],
1180 fuelInfo = [0x0b7c, 0x0b84, 0x0b94, 0x0b9c],
1181 numEngines = 2)
1182
1183 @property
1184 def name(self):
1185 """Get the name for this aircraft model."""
1186 return "FSUIPC/Generic Lisunov Li-2"
1187
1188#------------------------------------------------------------------------------
1189
1190class T134Model(GenericAircraftModel):
1191 """Generic model for the Tupolev Tu-134 aircraft."""
1192 def __init__(self):
1193 """Construct the model."""
1194 super(T134Model, self). \
1195 __init__(flapsNotches = [0, 10, 20, 30],
1196 fuelInfo = [0x0b74,
1197 0x0b8c, 0x0b84,
1198 0x0ba4, 0x0b9c,
1199 0x1254, 0x125c],
1200 numEngines = 2)
1201
1202 @property
1203 def name(self):
1204 """Get the name for this aircraft model."""
1205 return "FSUIPC/Generic Tupolev Tu-134"
1206
1207#------------------------------------------------------------------------------
1208
1209class T154Model(GenericAircraftModel):
1210 """Generic model for the Tupolev Tu-134 aircraft."""
1211 def __init__(self):
1212 """Construct the model."""
1213 super(T154Model, self). \
1214 __init__(flapsNotches = [0, 15, 28, 45],
1215 fuelInfo = [0x0b74, 0x0b7c, 0x0b94,
1216 0x1244, 0x0b84, 0x0b9c],
1217 numEngines = 3)
1218
1219 @property
1220 def name(self):
1221 """Get the name for this aircraft model."""
1222 return "FSUIPC/Generic Tupolev Tu-154"
1223
1224 def getAircraftState(self, aircraft, timestamp, data):
1225 """Get an aircraft state object for the given monitoring data.
1226
1227 This removes the reverser value for the middle engine."""
1228 state = super(T154Model, self).getAircraftState(aircraft, timestamp, data)
1229 del state.reverser[1]
1230 return state
1231
1232#------------------------------------------------------------------------------
1233
1234class YK40Model(GenericAircraftModel):
1235 """Generic model for the Yakovlev Yak-40 aircraft."""
1236 def __init__(self):
1237 """Construct the model."""
1238 super(YK40Model, self). \
1239 __init__(flapsNotches = [0, 20, 35],
1240 fuelInfo = [0x0b7c, 0x0b94],
1241 numEngines = 2)
1242
1243 @property
1244 def name(self):
1245 """Get the name for this aircraft model."""
1246 return "FSUIPC/Generic Yakovlev Yak-40"
1247
1248#------------------------------------------------------------------------------
1249
1250_genericModels = { const.AIRCRAFT_B736 : B737Model,
1251 const.AIRCRAFT_B737 : B737Model,
1252 const.AIRCRAFT_B738 : B737Model,
1253 const.AIRCRAFT_B733 : B737Model,
1254 const.AIRCRAFT_B734 : B737Model,
1255 const.AIRCRAFT_B735 : B737Model,
1256 const.AIRCRAFT_DH8D : DH8DModel,
1257 const.AIRCRAFT_B762 : B767Model,
1258 const.AIRCRAFT_B763 : B767Model,
1259 const.AIRCRAFT_CRJ2 : B767Model,
1260 const.AIRCRAFT_F70 : F70Model,
1261 const.AIRCRAFT_DC3 : DC3Model,
1262 const.AIRCRAFT_T134 : T134Model,
1263 const.AIRCRAFT_T154 : T154Model,
1264 const.AIRCRAFT_YK40 : YK40Model }
1265
1266#------------------------------------------------------------------------------
1267
1268AircraftModel.registerSpecial(PMDGBoeing737NGModel)
1269AircraftModel.registerSpecial(DreamwingsDH8DModel)
1270
1271#------------------------------------------------------------------------------
Note: See TracBrowser for help on using the repository browser.