source: src/fsuipc.py@ 18:8c35931dd0ae

Last change on this file since 18:8c35931dd0ae was 16:eb7e301a7d8b, checked in by István Váradi <ivaradi@…>, 13 years ago

Added a model handler for the Dreamwings Dash-8 and some other minor fixes

File size: 45.0 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
14
15if os.name == "nt":
16 import pyuipc
17else:
18 import pyuipc_emu as pyuipc
19
20#------------------------------------------------------------------------------
21
22class Handler(threading.Thread):
23 """The thread to handle the FSUIPC requests."""
24 @staticmethod
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
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
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):
70 """Process the request."""
71 if self._forWrite:
72 pyuipc.write(self._data)
73 Handler._callSafe(lambda: self._callback(True, self._extra))
74 else:
75 values = pyuipc.read(self._data)
76 Handler._callSafe(lambda: self._callback(values, self._extra))
77
78 return True
79
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
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
122 Handler._callSafe(lambda: self._callback(values, self._extra))
123
124 while self._nextFire <= time:
125 self._nextFire += self._period
126
127 return True
128
129 def fail(self):
130 """Handle the failure of this request."""
131 pass
132
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:
161 - the values retrieved or None on error
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
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!
182 """
183 with self._requestCondition:
184 self._requests.append(Handler.Request(True, data, callback, extra))
185 self._requestCondition.notify()
186
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
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_ANY)
279 description = "(FSUIPC version: 0x%04x, library version: 0x%04x, FS version: %d)" % \
280 (pyuipc.fsuipc_version, pyuipc.lib_version,
281 pyuipc.fs_version)
282 Handler._callSafe(lambda:
283 self._connectionListener.connected(const.SIM_MSFS9,
284 description))
285 return True
286 except Exception, e:
287 print "fsuipc.Handler._connect: connection failed: " + str(e)
288 time.sleep(0.1)
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()
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 = []
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 self._requestCondition.release()
330
331 try:
332 return request.process(time)
333 except Exception as e:
334 print "fsuipc.Handler._processRequest: FSUIPC connection failed (" + \
335 str(e) + ") reconnecting."
336 self._disconnect()
337 self._failRequests(request)
338 if not self._connect(): return None
339 else: return True
340 finally:
341 self._requestCondition.acquire()
342
343 def _processRequests(self):
344 """Process any pending requests.
345
346 Will be called with the request lock held."""
347 while self._connectionRequested and self._periodicRequests:
348 self._periodicRequests.sort()
349 request = self._periodicRequests[0]
350 result = self._processRequest(request, time.time())
351 if result is None: return False
352 elif not result: break
353
354 while self._connectionRequested and self._requests:
355 request = self._requests[0]
356 del self._requests[0]
357
358 if self._processRequest(request, None) is None:
359 return False
360
361 return self._connectionRequested
362
363#------------------------------------------------------------------------------
364
365class Simulator(object):
366 """The simulator class representing the interface to the flight simulator
367 via FSUIPC."""
368 # The basic data that should be queried all the time once we are connected
369 normalData = [ (0x0240, "H"), # Year
370 (0x023e, "H"), # Number of day in year
371 (0x023b, "b"), # UTC hour
372 (0x023c, "b"), # UTC minute
373 (0x023a, "b"), # seconds
374 (0x3d00, -256), # The name of the current aircraft
375 (0x3c00, -256) ] # The path of the current AIR file
376
377 flareData1 = [ (0x023a, "b"), # Seconds of time
378 (0x31e4, "d"), # Radio altitude
379 (0x02c8, "d") ] # Vertical speed
380
381 flareStartData = [ (0x0e90, "H"), # Ambient wind speed
382 (0x0e92, "H"), # Ambient wind direction
383 (0x0e8a, "H") ] # Visibility
384
385 flareData2 = [ (0x023a, "b"), # Seconds of time
386 (0x0366, "H"), # On the ground
387 (0x02c8, "d"), # Vertical speed
388 (0x030c, "d"), # Touch-down rate
389 (0x02bc, "d"), # IAS
390 (0x0578, "d"), # Pitch
391 (0x057c, "d"), # Bank
392 (0x0580, "d") ] # Heading
393
394 def __init__(self, connectionListener):
395 """Construct the simulator.
396
397 The aircraft object passed must provide the following members:
398 - type: one of the AIRCRAFT_XXX constants from const.py
399 - modelChanged(aircraftName, modelName): called when the model handling
400 the aircraft has changed.
401 - handleState(aircraftState): handle the given state.
402 - flareStarted(windSpeed, windDirection, visibility, flareStart,
403 flareStartFS): called when the flare has
404 started. windSpeed is in knots, windDirection is in degrees and
405 visibility is in metres. flareStart and flareStartFS are two time
406 values expressed in seconds that can be used to calculate the flare
407 time.
408 - flareFinished(flareEnd, flareEndFS, tdRate, tdRateCalculatedByFS,
409 ias, pitch, bank, heading): called when the flare has
410 finished, i.e. the aircraft is on the ground. flareEnd and flareEndFS
411 are the two time values corresponding to the touchdown time. tdRate is
412 the touch-down rate, tdRateCalculatedBySim indicates if the data comes
413 from the simulator or was calculated by the adapter. The other data
414 are self-explanatory and expressed in their 'natural' units."""
415 self._aircraft = None
416
417 self._handler = Handler(connectionListener)
418 self._handler.start()
419
420 self._normalRequestID = None
421
422 self._monitoringRequested = False
423 self._monitoring = False
424
425 self._aircraftName = None
426 self._aircraftModel = None
427
428 self._flareRequestID = None
429 self._flareRates = []
430 self._flareStart = None
431 self._flareStartFS = None
432
433 def connect(self, aircraft):
434 """Initiate a connection to the simulator."""
435 self._aircraft = aircraft
436 self._aircraftName = None
437 self._aircraftModel = None
438 self._handler.connect()
439 self._startDefaultNormal()
440
441 def startMonitoring(self):
442 """Start the periodic monitoring of the aircraft and pass the resulting
443 state to the aircraft object periodically."""
444 assert not self._monitoringRequested
445 self._monitoringRequested = True
446
447 def stopMonitoring(self):
448 """Stop the periodic monitoring of the aircraft."""
449 assert self._monitoringRequested
450 self._monitoringRequested = False
451
452 def startFlare(self):
453 """Start monitoring the flare time.
454
455 At present it is assumed to be called from the FSUIPC thread, hence no
456 protection."""
457 #self._aircraft.logger.debug("startFlare")
458 if self._flareRequestID is None:
459 self._flareRates = []
460 self._flareRequestID = self._handler.requestPeriodicRead(0.1,
461 Simulator.flareData1,
462 self._handleFlare1)
463
464 def cancelFlare(self):
465 """Cancel monitoring the flare time.
466
467 At present it is assumed to be called from the FSUIPC thread, hence no
468 protection."""
469 if self._flareRequestID is not None:
470 self._handler.clearPeriodic(self._flareRequestID)
471 self._flareRequestID = None
472
473 def disconnect(self):
474 """Disconnect from the simulator."""
475 assert not self._monitoringRequested
476
477 self._stopNormal()
478 self._handler.disconnect()
479
480 def _startDefaultNormal(self):
481 """Start the default normal periodic request."""
482 assert self._normalRequestID is None
483 self._normalRequestID = self._handler.requestPeriodicRead(1.0,
484 Simulator.normalData,
485 self._handleNormal)
486
487 def _stopNormal(self):
488 """Stop the normal period request."""
489 assert self._normalRequestID is not None
490 self._handler.clearPeriodic(self._normalRequestID)
491 self._normalRequestID = None
492 self._monitoring = False
493
494 def _handleNormal(self, data, extra):
495 """Handle the reply to the normal request.
496
497 At the beginning the result consists the data for normalData. When
498 monitoring is started, it contains the result also for the
499 aircraft-specific values.
500 """
501 timestamp = calendar.timegm(time.struct_time([data[0],
502 1, 1, 0, 0, 0, -1, 1, 0]))
503 timestamp += data[1] * 24 * 3600
504 timestamp += data[2] * 3600
505 timestamp += data[3] * 60
506 timestamp += data[4]
507
508 createdNewModel = self._setAircraftName(timestamp, data[5], data[6])
509
510 if self._monitoringRequested and not self._monitoring:
511 self._stopNormal()
512 self._startMonitoring()
513 elif self._monitoring and not self._monitoringRequested:
514 self._stopNormal()
515 self._startDefaultNormal()
516 elif self._monitoring and self._aircraftModel is not None and \
517 not createdNewModel:
518 aircraftState = self._aircraftModel.getAircraftState(self._aircraft,
519 timestamp, data)
520 self._aircraft.handleState(aircraftState)
521
522 def _setAircraftName(self, timestamp, name, airPath):
523 """Set the name of the aicraft and if it is different from the
524 previous, create a new model for it.
525
526 If so, also notifty the aircraft about the change.
527
528 Return if a new model was created."""
529 aircraftName = (name, airPath)
530 if aircraftName==self._aircraftName:
531 return False
532
533 self._aircraftName = aircraftName
534 needNew = self._aircraftModel is None
535 needNew = needNew or\
536 not self._aircraftModel.doesHandle(self._aircraft, aircraftName)
537 if not needNew:
538 specialModel = AircraftModel.findSpecial(self._aircraft, aircraftName)
539 needNew = specialModel is not None and \
540 specialModel is not self._aircraftModel.__class__
541
542 if needNew:
543 self._setAircraftModel(AircraftModel.create(self._aircraft, aircraftName))
544
545 self._aircraft.modelChanged(timestamp, name, self._aircraftModel.name)
546
547 return needNew
548
549 def _setAircraftModel(self, model):
550 """Set a new aircraft model.
551
552 It will be queried for the data to monitor and the monitoring request
553 will be replaced by a new one."""
554 self._aircraftModel = model
555
556 if self._monitoring:
557 self._stopNormal()
558 self._startMonitoring()
559
560 def _startMonitoring(self):
561 """Start monitoring with the current aircraft model."""
562 data = Simulator.normalData[:]
563 self._aircraftModel.addMonitoringData(data)
564
565 self._normalRequestID = \
566 self._handler.requestPeriodicRead(1.0, data,
567 self._handleNormal)
568 self._monitoring = True
569
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
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."""
637 monitoringData = [("paused", 0x0264, "H"),
638 ("frozen", 0x3364, "H"),
639 ("replay", 0x0628, "d"),
640 ("slew", 0x05dc, "H"),
641 ("overspeed", 0x036d, "b"),
642 ("stalled", 0x036c, "b"),
643 ("onTheGround", 0x0366, "H"),
644 ("zfw", 0x3bfc, "d"),
645 ("grossWeight", 0x30c0, "f"),
646 ("heading", 0x0580, "d"),
647 ("pitch", 0x0578, "d"),
648 ("bank", 0x057c, "d"),
649 ("ias", 0x02bc, "d"),
650 ("mach", 0x11c6, "H"),
651 ("groundSpeed", 0x02b4, "d"),
652 ("vs", 0x02c8, "d"),
653 ("radioAltitude", 0x31e4, "d"),
654 ("altitude", 0x0570, "l"),
655 ("gLoad", 0x11ba, "H"),
656 ("flapsControl", 0x0bdc, "d"),
657 ("flapsLeft", 0x0be0, "d"),
658 ("flapsRight", 0x0be4, "d"),
659 ("lights", 0x0d0c, "H"),
660 ("pitot", 0x029c, "b"),
661 ("parking", 0x0bc8, "H"),
662 ("noseGear", 0x0bec, "d"),
663 ("spoilersArmed", 0x0bcc, "d"),
664 ("spoilers", 0x0bd0, "d"),
665 ("altimeter", 0x0330, "H"),
666 ("nav1", 0x0350, "H"),
667 ("nav2", 0x0352, "H"),
668 ("squawk", 0x0354, "H"),
669 ("windSpeed", 0x0e90, "H"),
670 ("windDirection", 0x0e92, "H")]
671
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
680 @staticmethod
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
688 def create(aircraft, aircraftName):
689 """Create the model for the given aircraft name, and notify the
690 aircraft about it."""
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]()
696 else:
697 return GenericModel()
698
699 @staticmethod
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
711 def convertFrequency(data):
712 """Convert the given frequency data to a string."""
713 bcd = AircraftModel.convertBCD(data, 4)
714 return "1" + bcd[0:2] + "." + bcd[2:4]
715
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
727 def doesHandle(self, aircraft, aircraftName):
728 """Determine if the model handles the given aircraft name.
729
730 This default implementation returns False."""
731 return False
732
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:
738 setattr(self, attrName, len(dest)-1)
739
740 def _addDataWithIndexMembers(self, dest, prefix, data):
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:
752 self._addOffsetWithIndexMember(dest, offset, type, prefix + name)
753
754 def addMonitoringData(self, data):
755 """Add the model-specific monitoring data to the given array."""
756 self._addDataWithIndexMembers(data, "_monidx_",
757 AircraftModel.monitoringData)
758
759 def getAircraftState(self, aircraft, timestamp, data):
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 \
766 data[self._monidx_frozen]!=0 or \
767 data[self._monidx_replay]!=0
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
774 state.zfw = data[self._monidx_zfw] * const.LBSTOKG / 256.0
775 state.grossWeight = data[self._monidx_grossWeight] * const.LBSTOKG
776
777 state.heading = Handler.fsuipc2PositiveDegrees(data[self._monidx_heading])
778
779 state.pitch = Handler.fsuipc2Degrees(data[self._monidx_pitch])
780 state.bank = Handler.fsuipc2Degrees(data[self._monidx_bank])
781
782 state.ias = Handler.fsuipc2IAS(data[self._monidx_ias])
783 state.mach = data[self._monidx_mach] / 20480.0
784 state.groundSpeed = data[self._monidx_groundSpeed]* 3600.0/65536.0/1852.0
785 state.vs = Handler.fsuipc2VS(data[self._monidx_vs])
786
787 state.radioAltitude = \
788 Handler.fsuipc2radioAltitude(data[self._monidx_radioAltitude])
789 state.altitude = data[self._monidx_altitude]/const.FEETTOMETRES/65536.0/65536.0
790
791 state.gLoad = data[self._monidx_gLoad] / 625.0
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 state.flaps = self._flapsNotches[-1]*flapsLeft/16383.0
805
806 lights = data[self._monidx_lights]
807
808 state.navLightsOn = (lights&0x01) != 0
809 state.antiCollisionLightsOn = (lights&0x02) != 0
810 state.landingLightsOn = (lights&0x04) != 0
811 state.strobeLightsOn = (lights&0x10) != 0
812
813 state.pitotHeatOn = data[self._monidx_pitot]!=0
814
815 state.parking = data[self._monidx_parking]!=0
816
817 state.gearsDown = data[self._monidx_noseGear]==16383
818
819 state.spoilersArmed = data[self._monidx_spoilersArmed]!=0
820
821 spoilers = data[self._monidx_spoilers]
822 if spoilers<=4800:
823 state.spoilersExtension = 0.0
824 else:
825 state.spoilersExtension = (spoilers - 4800) * 100.0 / (16383 - 4800)
826
827 state.altimeter = data[self._monidx_altimeter] / 16.0
828
829 state.nav1 = AircraftModel.convertFrequency(data[self._monidx_nav1])
830 state.nav2 = AircraftModel.convertFrequency(data[self._monidx_nav2])
831 state.squawk = AircraftModel.convertBCD(data[self._monidx_squawk], 4)
832
833 state.windSpeed = data[self._monidx_windSpeed]
834 state.windDirection = data[self._monidx_windDirection]*360.0/65536.0
835 if state.windDirection<0.0: state.windDirection += 360.0
836
837 return state
838
839#------------------------------------------------------------------------------
840
841class GenericAircraftModel(AircraftModel):
842 """A generic aircraft model that can handle the fuel levels, the N1 or RPM
843 values and some other common parameters in a generic way."""
844 def __init__(self, flapsNotches, fuelInfo, numEngines, isN1 = True):
845 """Construct the generic aircraft model with the given data.
846
847 flapsNotches is an array of how much degrees the individual flaps
848 notches mean.
849
850 fuelInfo is an array of FSUIPC offsets for the levels of the fuel
851 tanks. It is assumed to be a 4-byte value, followed by another 4-byte
852 value, which is the fuel tank capacity.
853
854 numEngines is the number of engines the aircraft has.
855
856 isN1 determines if the engines have an N1 value or an RPM value
857 (e.g. pistons)."""
858 super(GenericAircraftModel, self).__init__(flapsNotches = flapsNotches)
859
860 self._fuelInfo = fuelInfo
861 self._fuelStartIndex = None
862 self._numEngines = numEngines
863 self._engineStartIndex = None
864 self._isN1 = isN1
865
866 def doesHandle(self, aircraft, aircraftName):
867 """Determine if the model handles the given aircraft name.
868
869 This implementation returns True."""
870 return True
871
872 def addMonitoringData(self, data):
873 """Add the model-specific monitoring data to the given array."""
874 super(GenericAircraftModel, self).addMonitoringData(data)
875
876 self._addOffsetWithIndexMember(data, 0x0af4, "H", "_monidx_fuelWeight")
877
878 self._fuelStartIndex = len(data)
879 for offset in self._fuelInfo:
880 self._addOffsetWithIndexMember(data, offset, "u") # tank level
881 self._addOffsetWithIndexMember(data, offset+4, "u") # tank capacity
882
883 if self._isN1:
884 self._engineStartIndex = len(data)
885 for i in range(0, self._numEngines):
886 self._addOffsetWithIndexMember(data, 0x0898 + i * 0x98, "H") # N1
887 self._addOffsetWithIndexMember(data, 0x088c + i * 0x98, "h") # throttle lever
888
889 def getAircraftState(self, aircraft, timestamp, data):
890 """Get the aircraft state.
891
892 Get it from the parent, and then add the data about the fuel levels and
893 the engine parameters."""
894 state = super(GenericAircraftModel, self).getAircraftState(aircraft,
895 timestamp,
896 data)
897
898 fuelWeight = data[self._monidx_fuelWeight]/256.0
899 state.fuel = []
900 for i in range(self._fuelStartIndex,
901 self._fuelStartIndex + 2*len(self._fuelInfo), 2):
902 fuel = data[i+1]*data[i]*fuelWeight*const.LBSTOKG/128.0/65536.0
903 state.fuel.append(fuel)
904
905 state.n1 = []
906 state.reverser = []
907 for i in range(self._engineStartIndex,
908 self._engineStartIndex + 2*self._numEngines, 2):
909 state.n1.append(data[i]*100.0/16384.0)
910 state.reverser.append(data[i+1]<0)
911
912 return state
913
914#------------------------------------------------------------------------------
915
916class GenericModel(GenericAircraftModel):
917 """Generic aircraft model for an unknown type."""
918 def __init__(self):
919 """Construct the model."""
920 super(GenericModel, self). \
921 __init__(flapsNotches = [0, 10, 20, 30],
922 fuelInfo = [0x0b74, 0x0b7c, 0xb94],
923 numEngines = 2)
924
925 @property
926 def name(self):
927 """Get the name for this aircraft model."""
928 return "FSUIPC/Generic"
929
930#------------------------------------------------------------------------------
931
932class B737Model(GenericAircraftModel):
933 """Generic model for the Boeing 737 Classing and NG aircraft."""
934 def __init__(self):
935 """Construct the model."""
936 super(B737Model, self). \
937 __init__(flapsNotches = [0, 1, 2, 5, 10, 15, 25, 30, 40],
938 fuelInfo = [0x0b74, 0x0b7c, 0xb94],
939 numEngines = 2)
940
941 @property
942 def name(self):
943 """Get the name for this aircraft model."""
944 return "FSUIPC/Generic Boeing 737"
945
946#------------------------------------------------------------------------------
947
948class PMDGBoeing737NGModel(B737Model):
949 """A model handler for the PMDG Boeing 737NG model."""
950 @staticmethod
951 def doesHandle(aircraft, (name, airPath)):
952 """Determine if this model handler handles the aircraft with the given
953 name."""
954 return aircraft.type in [const.AIRCRAFT_B736,
955 const.AIRCRAFT_B737,
956 const.AIRCRAFT_B738] and \
957 (name.find("PMDG")!=-1 or airPath.find("PMDG")!=-1) and \
958 (name.find("737")!=-1 or airPath.find("737")!=-1) and \
959 (name.find("600")!=-1 or airPath.find("600")!=-1 or \
960 name.find("700")!=-1 or airPath.find("700")!=-1 or \
961 name.find("800")!=-1 or airPath.find("800")!=-1 or \
962 name.find("900")!=-1 or airPath.find("900")!=-1)
963
964 @property
965 def name(self):
966 """Get the name for this aircraft model."""
967 return "FSUIPC/PMDG Boeing 737NG"
968
969 def addMonitoringData(self, data):
970 """Add the model-specific monitoring data to the given array."""
971 super(PMDGBoeing737NGModel, self).addMonitoringData(data)
972
973 self._addOffsetWithIndexMember(data, 0x6202, "b", "_pmdgidx_switches")
974
975 def getAircraftState(self, aircraft, timestamp, data):
976 """Get the aircraft state.
977
978 Get it from the parent, and then check some PMDG-specific stuff."""
979 state = super(PMDGBoeing737NGModel, self).getAircraftState(aircraft,
980 timestamp,
981 data)
982 if data[self._pmdgidx_switches]&0x01==0x01:
983 state.altimeter = 1013.25
984
985 return state
986
987#------------------------------------------------------------------------------
988
989class B767Model(GenericAircraftModel):
990 """Generic model for the Boeing 767 aircraft."""
991 def __init__(self):
992 """Construct the model."""
993 super(B767Model, self). \
994 __init__(flapsNotches = [0, 1, 5, 15, 20, 25, 30],
995 fuelInfo = [0x0b74, 0x0b7c, 0xb94],
996 numEngines = 2)
997
998 @property
999 def name(self):
1000 """Get the name for this aircraft model."""
1001 return "FSUIPC/Generic Boeing 767"
1002
1003#------------------------------------------------------------------------------
1004
1005class DH8DModel(GenericAircraftModel):
1006 """Generic model for the Bombardier Dash 8-Q400 aircraft."""
1007 def __init__(self):
1008 """Construct the model."""
1009 super(DH8DModel, self). \
1010 __init__(flapsNotches = [0, 5, 10, 15, 35],
1011 fuelInfo = [0x0b74, 0x0b7c, 0xb94],
1012 numEngines = 2)
1013
1014 @property
1015 def name(self):
1016 """Get the name for this aircraft model."""
1017 return "FSUIPC/Generic Bombardier Dash 8-Q400"
1018
1019#------------------------------------------------------------------------------
1020
1021class DreamwingsDH8DModel(DH8DModel):
1022 """Model handler for the Dreamwings Dash 8-Q400."""
1023 @staticmethod
1024 def doesHandle(aircraft, (name, airPath)):
1025 """Determine if this model handler handles the aircraft with the given
1026 name."""
1027 return aircraft.type==const.AIRCRAFT_DH8D and \
1028 (name.find("Dreamwings")!=-1 or airPath.find("Dreamwings")!=-1) and \
1029 (name.find("Dash")!=-1 or airPath.find("Dash")!=-1) and \
1030 (name.find("Q400")!=-1 or airPath.find("Q400")!=-1) and \
1031 airPath.find("Dash8Q400")!=-1
1032
1033 @property
1034 def name(self):
1035 """Get the name for this aircraft model."""
1036 return "FSUIPC/Dreamwings Bombardier Dash 8-Q400"
1037
1038 def getAircraftState(self, aircraft, timestamp, data):
1039 """Get the aircraft state.
1040
1041 Get it from the parent, and then invert the pitot heat state."""
1042 state = super(DreamwingsDH8DModel, self).getAircraftState(aircraft,
1043 timestamp,
1044 data)
1045 state.pitotHeatOn = not state.pitotHeatOn
1046
1047 return state
1048#------------------------------------------------------------------------------
1049
1050class CRJ2Model(GenericAircraftModel):
1051 """Generic model for the Bombardier CRJ-200 aircraft."""
1052 def __init__(self):
1053 """Construct the model."""
1054 super(CRJ2Model, self). \
1055 __init__(flapsNotches = [0, 8, 20, 30, 45],
1056 fuelInfo = [0x0b74, 0x0b7c, 0xb94],
1057 numEngines = 2)
1058
1059 @property
1060 def name(self):
1061 """Get the name for this aircraft model."""
1062 return "FSUIPC/Generic Bombardier CRJ-200"
1063
1064#------------------------------------------------------------------------------
1065
1066class F70Model(GenericAircraftModel):
1067 """Generic model for the Fokker F70 aircraft."""
1068 def __init__(self):
1069 """Construct the model."""
1070 super(F70Model, self). \
1071 __init__(flapsNotches = [0, 8, 15, 25, 42],
1072 fuelInfo = [0x0b74, 0x0b7c, 0xb94],
1073 numEngines = 2)
1074
1075 @property
1076 def name(self):
1077 """Get the name for this aircraft model."""
1078 return "FSUIPC/Generic Fokker 70"
1079
1080#------------------------------------------------------------------------------
1081
1082class DC3Model(GenericAircraftModel):
1083 """Generic model for the Lisunov Li-2 (DC-3) aircraft."""
1084 def __init__(self):
1085 """Construct the model."""
1086 super(DC3Model, self). \
1087 __init__(flapsNotches = [0, 15, 30, 45],
1088 fuelInfo = [0x0b7c, 0x0b84, 0x0b94, 0x0b9c],
1089 numEngines = 2)
1090
1091 @property
1092 def name(self):
1093 """Get the name for this aircraft model."""
1094 return "FSUIPC/Generic Lisunov Li-2"
1095
1096#------------------------------------------------------------------------------
1097
1098class T134Model(GenericAircraftModel):
1099 """Generic model for the Tupolev Tu-134 aircraft."""
1100 def __init__(self):
1101 """Construct the model."""
1102 super(T134Model, self). \
1103 __init__(flapsNotches = [0, 10, 20, 30],
1104 fuelInfo = [0x0b74,
1105 0x0b8c, 0x0b84,
1106 0x0ba4, 0x0b9c,
1107 0x1254, 0x125c],
1108 numEngines = 2)
1109
1110 @property
1111 def name(self):
1112 """Get the name for this aircraft model."""
1113 return "FSUIPC/Generic Tupolev Tu-134"
1114
1115#------------------------------------------------------------------------------
1116
1117class T154Model(GenericAircraftModel):
1118 """Generic model for the Tupolev Tu-134 aircraft."""
1119 def __init__(self):
1120 """Construct the model."""
1121 super(T154Model, self). \
1122 __init__(flapsNotches = [0, 15, 28, 45],
1123 fuelInfo = [0x0b74, 0x0b7c, 0x0b94,
1124 0x1244, 0x0b84, 0x0b9c],
1125 numEngines = 3)
1126
1127 @property
1128 def name(self):
1129 """Get the name for this aircraft model."""
1130 return "FSUIPC/Generic Tupolev Tu-154"
1131
1132 def getAircraftState(self, aircraft, timestamp, data):
1133 """Get an aircraft state object for the given monitoring data.
1134
1135 This removes the reverser value for the middle engine."""
1136 state = super(T154Model, self).getAircraftState(aircraft, timestamp, data)
1137 del state.reverser[1]
1138 return state
1139
1140#------------------------------------------------------------------------------
1141
1142class YK40Model(GenericAircraftModel):
1143 """Generic model for the Yakovlev Yak-40 aircraft."""
1144 def __init__(self):
1145 """Construct the model."""
1146 super(YK40Model, self). \
1147 __init__(flapsNotches = [0, 20, 35],
1148 fuelInfo = [0x0b7c, 0x0b94],
1149 numEngines = 2)
1150
1151 @property
1152 def name(self):
1153 """Get the name for this aircraft model."""
1154 return "FSUIPC/Generic Yakovlev Yak-40"
1155
1156#------------------------------------------------------------------------------
1157
1158_genericModels = { const.AIRCRAFT_B736 : B737Model,
1159 const.AIRCRAFT_B737 : B737Model,
1160 const.AIRCRAFT_B738 : B737Model,
1161 const.AIRCRAFT_B733 : B737Model,
1162 const.AIRCRAFT_B734 : B737Model,
1163 const.AIRCRAFT_B735 : B737Model,
1164 const.AIRCRAFT_DH8D : DH8DModel,
1165 const.AIRCRAFT_B762 : B767Model,
1166 const.AIRCRAFT_B763 : B767Model,
1167 const.AIRCRAFT_CRJ2 : B767Model,
1168 const.AIRCRAFT_F70 : F70Model,
1169 const.AIRCRAFT_DC3 : DC3Model,
1170 const.AIRCRAFT_T134 : T134Model,
1171 const.AIRCRAFT_T154 : T154Model,
1172 const.AIRCRAFT_YK40 : YK40Model }
1173
1174#------------------------------------------------------------------------------
1175
1176AircraftModel.registerSpecial(PMDGBoeing737NGModel)
1177AircraftModel.registerSpecial(DreamwingsDH8DModel)
1178
1179#------------------------------------------------------------------------------
Note: See TracBrowser for help on using the repository browser.