source: src/mlx/fsuipc.py@ 117:af3d52b9adc4

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

Implemented the weight help tab

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