source: src/fsuipc.py@ 21:0b447152d28b

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

Added support for validating the data read

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