source: src/fsuipc.py@ 10:103229d7ace5

Last change on this file since 10:103229d7ace5 was 9:3dac12e8914d, checked in by István Váradi <ivaradi@…>, 13 years ago

Flare calculations are handled and added some other printouts

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