source: src/mlx/fsuipc.py@ 43:f5c93554d51c

Last change on this file since 43:f5c93554d51c was 27:d95d48685969, checked in by István Váradi <ivaradi@…>, 13 years ago

Created icons, reorganized sources and made the Windows installer work

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