source: src/mlx/fsuipc.py@ 115:fa8178825b29

Last change on this file since 115:fa8178825b29 was 97:f885322fb296, checked in by István Váradi <ivaradi@…>, 13 years ago

The PIREP can be created and sent.

File size: 49.7 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 requestTime(self, callback):
534 """Request the time from the simulator."""
535 self._handler.requestRead(Simulator.timeData, self._handleTime,
536 extra = callback)
537
538 def startMonitoring(self):
539 """Start the periodic monitoring of the aircraft and pass the resulting
540 state to the aircraft object periodically."""
541 assert not self._monitoringRequested
542 self._monitoringRequested = True
543
544 def stopMonitoring(self):
545 """Stop the periodic monitoring of the aircraft."""
546 assert self._monitoringRequested
547 self._monitoringRequested = False
548
549 def startFlare(self):
550 """Start monitoring the flare time.
551
552 At present it is assumed to be called from the FSUIPC thread, hence no
553 protection."""
554 #self._aircraft.logger.debug("startFlare")
555 if self._flareRequestID is None:
556 self._flareRates = []
557 self._flareRequestID = self._handler.requestPeriodicRead(0.1,
558 Simulator.flareData1,
559 self._handleFlare1)
560
561 def cancelFlare(self):
562 """Cancel monitoring the flare time.
563
564 At present it is assumed to be called from the FSUIPC thread, hence no
565 protection."""
566 if self._flareRequestID is not None:
567 self._handler.clearPeriodic(self._flareRequestID)
568 self._flareRequestID = None
569
570 def disconnect(self):
571 """Disconnect from the simulator."""
572 assert not self._monitoringRequested
573
574 self._stopNormal()
575 self._handler.disconnect()
576
577 def _startDefaultNormal(self):
578 """Start the default normal periodic request."""
579 assert self._normalRequestID is None
580 self._normalRequestID = \
581 self._handler.requestPeriodicRead(1.0,
582 Simulator.normalData,
583 self._handleNormal,
584 validator = self._validateNormal)
585
586 def _stopNormal(self):
587 """Stop the normal period request."""
588 assert self._normalRequestID is not None
589 self._handler.clearPeriodic(self._normalRequestID)
590 self._normalRequestID = None
591 self._monitoring = False
592
593 def _validateNormal(self, data, extra):
594 """Validate the normal data."""
595 return data[0]!=0 and data[1]!=0 and len(data[5])>0 and len(data[6])>0
596
597 def _handleNormal(self, data, extra):
598 """Handle the reply to the normal request.
599
600 At the beginning the result consists the data for normalData. When
601 monitoring is started, it contains the result also for the
602 aircraft-specific values.
603 """
604 timestamp = Simulator._getTimestamp(data)
605
606 createdNewModel = self._setAircraftName(timestamp, data[5], data[6])
607
608 if self._monitoringRequested and not self._monitoring:
609 self._stopNormal()
610 self._startMonitoring()
611 elif self._monitoring and not self._monitoringRequested:
612 self._stopNormal()
613 self._startDefaultNormal()
614 elif self._monitoring and self._aircraftModel is not None and \
615 not createdNewModel:
616 aircraftState = self._aircraftModel.getAircraftState(self._aircraft,
617 timestamp, data)
618 self._aircraft.handleState(aircraftState)
619
620 def _setAircraftName(self, timestamp, name, airPath):
621 """Set the name of the aicraft and if it is different from the
622 previous, create a new model for it.
623
624 If so, also notifty the aircraft about the change.
625
626 Return if a new model was created."""
627 aircraftName = (name, airPath)
628 if aircraftName==self._aircraftName:
629 return False
630
631 self._aircraftName = aircraftName
632 needNew = self._aircraftModel is None
633 needNew = needNew or\
634 not self._aircraftModel.doesHandle(self._aircraft, aircraftName)
635 if not needNew:
636 specialModel = AircraftModel.findSpecial(self._aircraft, aircraftName)
637 needNew = specialModel is not None and \
638 specialModel is not self._aircraftModel.__class__
639
640 if needNew:
641 self._setAircraftModel(AircraftModel.create(self._aircraft, aircraftName))
642
643
644 self._aircraft.modelChanged(timestamp, self._latin1decoder(name)[0],
645 self._aircraftModel.name)
646
647 return needNew
648
649 def _setAircraftModel(self, model):
650 """Set a new aircraft model.
651
652 It will be queried for the data to monitor and the monitoring request
653 will be replaced by a new one."""
654 self._aircraftModel = model
655
656 if self._monitoring:
657 self._stopNormal()
658 self._startMonitoring()
659
660 def _startMonitoring(self):
661 """Start monitoring with the current aircraft model."""
662 data = Simulator.normalData[:]
663 self._aircraftModel.addMonitoringData(data)
664
665 self._normalRequestID = \
666 self._handler.requestPeriodicRead(1.0, data,
667 self._handleNormal,
668 validator = self._validateNormal)
669 self._monitoring = True
670
671 def _addFlareRate(self, data):
672 """Append a flare rate to the list of last rates."""
673 if len(self._flareRates)>=3:
674 del self._flareRates[0]
675 self._flareRates.append(Handler.fsuipc2VS(data))
676
677 def _handleFlare1(self, data, normal):
678 """Handle the first stage of flare monitoring."""
679 #self._aircraft.logger.debug("handleFlare1: " + str(data))
680 if Handler.fsuipc2radioAltitude(data[1])<=50.0:
681 self._flareStart = time.time()
682 self._flareStartFS = data[0]
683 self._handler.clearPeriodic(self._flareRequestID)
684 self._flareRequestID = \
685 self._handler.requestPeriodicRead(0.1,
686 Simulator.flareData2,
687 self._handleFlare2)
688 self._handler.requestRead(Simulator.flareStartData,
689 self._handleFlareStart)
690
691 self._addFlareRate(data[2])
692
693 def _handleFlareStart(self, data, extra):
694 """Handle the data need to notify the aircraft about the starting of
695 the flare."""
696 #self._aircraft.logger.debug("handleFlareStart: " + str(data))
697 if data is not None:
698 windDirection = data[1]*360.0/65536.0
699 if windDirection<0.0: windDirection += 360.0
700 self._aircraft.flareStarted(data[0], windDirection,
701 data[2]*1609.344/100.0,
702 self._flareStart, self._flareStartFS)
703
704 def _handleFlare2(self, data, normal):
705 """Handle the first stage of flare monitoring."""
706 #self._aircraft.logger.debug("handleFlare2: " + str(data))
707 if data[1]!=0:
708 flareEnd = time.time()
709 self._handler.clearPeriodic(self._flareRequestID)
710 self._flareRequestID = None
711
712 flareEndFS = data[0]
713 if flareEndFS<self._flareStartFS:
714 flareEndFS += 60
715
716 tdRate = Handler.fsuipc2VS(data[3])
717 tdRateCalculatedByFS = True
718 if tdRate==0 or tdRate>1000.0 or tdRate<-1000.0:
719 tdRate = min(self._flareRates)
720 tdRateCalculatedByFS = False
721
722 self._aircraft.flareFinished(flareEnd, flareEndFS,
723 tdRate, tdRateCalculatedByFS,
724 Handler.fsuipc2IAS(data[4]),
725 Handler.fsuipc2Degrees(data[5]),
726 Handler.fsuipc2Degrees(data[6]),
727 Handler.fsuipc2PositiveDegrees(data[7]))
728 else:
729 self._addFlareRate(data[2])
730
731 def _handleZFW(self, data, callback):
732 """Callback for a ZFW retrieval request."""
733 zfw = data[0] * const.LBSTOKG / 256.0
734 callback(zfw)
735
736 def _handleTime(self, data, callback):
737 """Callback for a time retrieval request."""
738 callback(Simulator._getTimestamp(data))
739
740#------------------------------------------------------------------------------
741
742class AircraftModel(object):
743 """Base class for the aircraft models.
744
745 Aircraft models handle the data arriving from FSUIPC and turn it into an
746 object describing the aircraft's state."""
747 monitoringData = [("paused", 0x0264, "H"),
748 ("latitude", 0x0560, "l"),
749 ("longitude", 0x0568, "l"),
750 ("frozen", 0x3364, "H"),
751 ("replay", 0x0628, "d"),
752 ("slew", 0x05dc, "H"),
753 ("overspeed", 0x036d, "b"),
754 ("stalled", 0x036c, "b"),
755 ("onTheGround", 0x0366, "H"),
756 ("zfw", 0x3bfc, "d"),
757 ("grossWeight", 0x30c0, "f"),
758 ("heading", 0x0580, "d"),
759 ("pitch", 0x0578, "d"),
760 ("bank", 0x057c, "d"),
761 ("ias", 0x02bc, "d"),
762 ("mach", 0x11c6, "H"),
763 ("groundSpeed", 0x02b4, "d"),
764 ("vs", 0x02c8, "d"),
765 ("radioAltitude", 0x31e4, "d"),
766 ("altitude", 0x0570, "l"),
767 ("gLoad", 0x11ba, "H"),
768 ("flapsControl", 0x0bdc, "d"),
769 ("flapsLeft", 0x0be0, "d"),
770 ("flapsRight", 0x0be4, "d"),
771 ("lights", 0x0d0c, "H"),
772 ("pitot", 0x029c, "b"),
773 ("parking", 0x0bc8, "H"),
774 ("noseGear", 0x0bec, "d"),
775 ("spoilersArmed", 0x0bcc, "d"),
776 ("spoilers", 0x0bd0, "d"),
777 ("altimeter", 0x0330, "H"),
778 ("nav1", 0x0350, "H"),
779 ("nav2", 0x0352, "H"),
780 ("squawk", 0x0354, "H"),
781 ("windSpeed", 0x0e90, "H"),
782 ("windDirection", 0x0e92, "H")]
783
784
785 specialModels = []
786
787 @staticmethod
788 def registerSpecial(clazz):
789 """Register the given class as a special model."""
790 AircraftModel.specialModels.append(clazz)
791
792 @staticmethod
793 def findSpecial(aircraft, aircraftName):
794 for specialModel in AircraftModel.specialModels:
795 if specialModel.doesHandle(aircraft, aircraftName):
796 return specialModel
797 return None
798
799 @staticmethod
800 def create(aircraft, aircraftName):
801 """Create the model for the given aircraft name, and notify the
802 aircraft about it."""
803 specialModel = AircraftModel.findSpecial(aircraft, aircraftName)
804 if specialModel is not None:
805 return specialModel()
806 if aircraft.type in _genericModels:
807 return _genericModels[aircraft.type]()
808 else:
809 return GenericModel()
810
811 @staticmethod
812 def convertBCD(data, length):
813 """Convert a data item encoded as BCD into a string of the given number
814 of digits."""
815 bcd = ""
816 for i in range(0, length):
817 digit = chr(ord('0') + (data&0x0f))
818 data >>= 4
819 bcd = digit + bcd
820 return bcd
821
822 @staticmethod
823 def convertFrequency(data):
824 """Convert the given frequency data to a string."""
825 bcd = AircraftModel.convertBCD(data, 4)
826 return "1" + bcd[0:2] + "." + bcd[2:4]
827
828 def __init__(self, flapsNotches):
829 """Construct the aircraft model.
830
831 flapsNotches is a list of degrees of flaps that are available on the aircraft."""
832 self._flapsNotches = flapsNotches
833
834 @property
835 def name(self):
836 """Get the name for this aircraft model."""
837 return "FSUIPC/Generic"
838
839 def doesHandle(self, aircraft, aircraftName):
840 """Determine if the model handles the given aircraft name.
841
842 This default implementation returns False."""
843 return False
844
845 def _addOffsetWithIndexMember(self, dest, offset, type, attrName = None):
846 """Add the given FSUIPC offset and type to the given array and a member
847 attribute with the given name."""
848 dest.append((offset, type))
849 if attrName is not None:
850 setattr(self, attrName, len(dest)-1)
851
852 def _addDataWithIndexMembers(self, dest, prefix, data):
853 """Add FSUIPC data to the given array and also corresponding index
854 member variables with the given prefix.
855
856 data is a list of triplets of the following items:
857 - the name of the data item. The index member variable will have a name
858 created by prepending the given prefix to this name.
859 - the FSUIPC offset
860 - the FSUIPC type
861
862 The latter two items will be appended to dest."""
863 for (name, offset, type) in data:
864 self._addOffsetWithIndexMember(dest, offset, type, prefix + name)
865
866 def addMonitoringData(self, data):
867 """Add the model-specific monitoring data to the given array."""
868 self._addDataWithIndexMembers(data, "_monidx_",
869 AircraftModel.monitoringData)
870
871 def getAircraftState(self, aircraft, timestamp, data):
872 """Get an aircraft state object for the given monitoring data."""
873 state = fs.AircraftState()
874
875 state.timestamp = timestamp
876
877 state.latitude = data[self._monidx_latitude] * \
878 90.0 / 10001750.0 / 65536.0 / 65536.0
879
880 state.longitude = data[self._monidx_longitude] * \
881 360.0 / 65536.0 / 65536.0 / 65536.0 / 65536.0
882 if state.longitude>180.0: state.longitude = 360.0 - state.longitude
883
884 state.paused = data[self._monidx_paused]!=0 or \
885 data[self._monidx_frozen]!=0 or \
886 data[self._monidx_replay]!=0
887 state.trickMode = data[self._monidx_slew]!=0
888
889 state.overspeed = data[self._monidx_overspeed]!=0
890 state.stalled = data[self._monidx_stalled]!=0
891 state.onTheGround = data[self._monidx_onTheGround]!=0
892
893 state.zfw = data[self._monidx_zfw] * const.LBSTOKG / 256.0
894 state.grossWeight = data[self._monidx_grossWeight] * const.LBSTOKG
895
896 state.heading = Handler.fsuipc2PositiveDegrees(data[self._monidx_heading])
897
898 state.pitch = Handler.fsuipc2Degrees(data[self._monidx_pitch])
899 state.bank = Handler.fsuipc2Degrees(data[self._monidx_bank])
900
901 state.ias = Handler.fsuipc2IAS(data[self._monidx_ias])
902 state.mach = data[self._monidx_mach] / 20480.0
903 state.groundSpeed = data[self._monidx_groundSpeed]* 3600.0/65536.0/1852.0
904 state.vs = Handler.fsuipc2VS(data[self._monidx_vs])
905
906 state.radioAltitude = \
907 Handler.fsuipc2radioAltitude(data[self._monidx_radioAltitude])
908 state.altitude = data[self._monidx_altitude]/const.FEETTOMETRES/65536.0/65536.0
909
910 state.gLoad = data[self._monidx_gLoad] / 625.0
911
912 numNotchesM1 = len(self._flapsNotches) - 1
913 flapsIncrement = 16383 / numNotchesM1
914 flapsControl = data[self._monidx_flapsControl]
915 flapsIndex = flapsControl / flapsIncrement
916 if flapsIndex < numNotchesM1:
917 if (flapsControl - (flapsIndex*flapsIncrement) >
918 (flapsIndex+1)*flapsIncrement - flapsControl):
919 flapsIndex += 1
920 state.flapsSet = self._flapsNotches[flapsIndex]
921
922 flapsLeft = data[self._monidx_flapsLeft]
923 state.flaps = self._flapsNotches[-1]*flapsLeft/16383.0
924
925 lights = data[self._monidx_lights]
926
927 state.navLightsOn = (lights&0x01) != 0
928 state.antiCollisionLightsOn = (lights&0x02) != 0
929 state.landingLightsOn = (lights&0x04) != 0
930 state.strobeLightsOn = (lights&0x10) != 0
931
932 state.pitotHeatOn = data[self._monidx_pitot]!=0
933
934 state.parking = data[self._monidx_parking]!=0
935
936 state.gearsDown = data[self._monidx_noseGear]==16383
937
938 state.spoilersArmed = data[self._monidx_spoilersArmed]!=0
939
940 spoilers = data[self._monidx_spoilers]
941 if spoilers<=4800:
942 state.spoilersExtension = 0.0
943 else:
944 state.spoilersExtension = (spoilers - 4800) * 100.0 / (16383 - 4800)
945
946 state.altimeter = data[self._monidx_altimeter] / 16.0
947
948 state.nav1 = AircraftModel.convertFrequency(data[self._monidx_nav1])
949 state.nav2 = AircraftModel.convertFrequency(data[self._monidx_nav2])
950 state.squawk = AircraftModel.convertBCD(data[self._monidx_squawk], 4)
951
952 state.windSpeed = data[self._monidx_windSpeed]
953 state.windDirection = data[self._monidx_windDirection]*360.0/65536.0
954 if state.windDirection<0.0: state.windDirection += 360.0
955
956 return state
957
958#------------------------------------------------------------------------------
959
960class GenericAircraftModel(AircraftModel):
961 """A generic aircraft model that can handle the fuel levels, the N1 or RPM
962 values and some other common parameters in a generic way."""
963 def __init__(self, flapsNotches, fuelInfo, numEngines, isN1 = True):
964 """Construct the generic aircraft model with the given data.
965
966 flapsNotches is an array of how much degrees the individual flaps
967 notches mean.
968
969 fuelInfo is an array of FSUIPC offsets for the levels of the fuel
970 tanks. It is assumed to be a 4-byte value, followed by another 4-byte
971 value, which is the fuel tank capacity.
972
973 numEngines is the number of engines the aircraft has.
974
975 isN1 determines if the engines have an N1 value or an RPM value
976 (e.g. pistons)."""
977 super(GenericAircraftModel, self).__init__(flapsNotches = flapsNotches)
978
979 self._fuelInfo = fuelInfo
980 self._fuelStartIndex = None
981 self._numEngines = numEngines
982 self._engineStartIndex = None
983 self._isN1 = isN1
984
985 def doesHandle(self, aircraft, aircraftName):
986 """Determine if the model handles the given aircraft name.
987
988 This implementation returns True."""
989 return True
990
991 def addMonitoringData(self, data):
992 """Add the model-specific monitoring data to the given array."""
993 super(GenericAircraftModel, self).addMonitoringData(data)
994
995 self._addOffsetWithIndexMember(data, 0x0af4, "H", "_monidx_fuelWeight")
996
997 self._fuelStartIndex = len(data)
998 for offset in self._fuelInfo:
999 self._addOffsetWithIndexMember(data, offset, "u") # tank level
1000 self._addOffsetWithIndexMember(data, offset+4, "u") # tank capacity
1001
1002 if self._isN1:
1003 self._engineStartIndex = len(data)
1004 for i in range(0, self._numEngines):
1005 self._addOffsetWithIndexMember(data, 0x2000 + i * 0x100, "f") # N1
1006 self._addOffsetWithIndexMember(data, 0x088c + i * 0x98, "h") # throttle lever
1007
1008 def getAircraftState(self, aircraft, timestamp, data):
1009 """Get the aircraft state.
1010
1011 Get it from the parent, and then add the data about the fuel levels and
1012 the engine parameters."""
1013 state = super(GenericAircraftModel, self).getAircraftState(aircraft,
1014 timestamp,
1015 data)
1016
1017 fuelWeight = data[self._monidx_fuelWeight]/256.0
1018 state.fuel = []
1019 for i in range(self._fuelStartIndex,
1020 self._fuelStartIndex + 2*len(self._fuelInfo), 2):
1021 fuel = data[i+1]*data[i]*fuelWeight*const.LBSTOKG/128.0/65536.0
1022 state.fuel.append(fuel)
1023
1024 state.n1 = []
1025 state.reverser = []
1026 for i in range(self._engineStartIndex,
1027 self._engineStartIndex + 2*self._numEngines, 2):
1028 state.n1.append(data[i])
1029 state.reverser.append(data[i+1]<0)
1030
1031 return state
1032
1033#------------------------------------------------------------------------------
1034
1035class GenericModel(GenericAircraftModel):
1036 """Generic aircraft model for an unknown type."""
1037 def __init__(self):
1038 """Construct the model."""
1039 super(GenericModel, self). \
1040 __init__(flapsNotches = [0, 10, 20, 30],
1041 fuelInfo = [0x0b74, 0x0b7c, 0xb94],
1042 numEngines = 2)
1043
1044 @property
1045 def name(self):
1046 """Get the name for this aircraft model."""
1047 return "FSUIPC/Generic"
1048
1049#------------------------------------------------------------------------------
1050
1051class B737Model(GenericAircraftModel):
1052 """Generic model for the Boeing 737 Classing and NG aircraft."""
1053 def __init__(self):
1054 """Construct the model."""
1055 super(B737Model, self). \
1056 __init__(flapsNotches = [0, 1, 2, 5, 10, 15, 25, 30, 40],
1057 fuelInfo = [0x0b74, 0x0b7c, 0xb94],
1058 numEngines = 2)
1059
1060 @property
1061 def name(self):
1062 """Get the name for this aircraft model."""
1063 return "FSUIPC/Generic Boeing 737"
1064
1065#------------------------------------------------------------------------------
1066
1067class PMDGBoeing737NGModel(B737Model):
1068 """A model handler for the PMDG Boeing 737NG model."""
1069 @staticmethod
1070 def doesHandle(aircraft, (name, airPath)):
1071 """Determine if this model handler handles the aircraft with the given
1072 name."""
1073 return aircraft.type in [const.AIRCRAFT_B736,
1074 const.AIRCRAFT_B737,
1075 const.AIRCRAFT_B738] and \
1076 (name.find("PMDG")!=-1 or airPath.find("PMDG")!=-1) and \
1077 (name.find("737")!=-1 or airPath.find("737")!=-1) and \
1078 (name.find("600")!=-1 or airPath.find("600")!=-1 or \
1079 name.find("700")!=-1 or airPath.find("700")!=-1 or \
1080 name.find("800")!=-1 or airPath.find("800")!=-1 or \
1081 name.find("900")!=-1 or airPath.find("900")!=-1)
1082
1083 @property
1084 def name(self):
1085 """Get the name for this aircraft model."""
1086 return "FSUIPC/PMDG Boeing 737NG"
1087
1088 def addMonitoringData(self, data):
1089 """Add the model-specific monitoring data to the given array."""
1090 super(PMDGBoeing737NGModel, self).addMonitoringData(data)
1091
1092 self._addOffsetWithIndexMember(data, 0x6202, "b", "_pmdgidx_switches")
1093
1094 def getAircraftState(self, aircraft, timestamp, data):
1095 """Get the aircraft state.
1096
1097 Get it from the parent, and then check some PMDG-specific stuff."""
1098 state = super(PMDGBoeing737NGModel, self).getAircraftState(aircraft,
1099 timestamp,
1100 data)
1101 if data[self._pmdgidx_switches]&0x01==0x01:
1102 state.altimeter = 1013.25
1103
1104 return state
1105
1106#------------------------------------------------------------------------------
1107
1108class B767Model(GenericAircraftModel):
1109 """Generic model for the Boeing 767 aircraft."""
1110 def __init__(self):
1111 """Construct the model."""
1112 super(B767Model, self). \
1113 __init__(flapsNotches = [0, 1, 5, 15, 20, 25, 30],
1114 fuelInfo = [0x0b74, 0x0b7c, 0xb94],
1115 numEngines = 2)
1116
1117 @property
1118 def name(self):
1119 """Get the name for this aircraft model."""
1120 return "FSUIPC/Generic Boeing 767"
1121
1122#------------------------------------------------------------------------------
1123
1124class DH8DModel(GenericAircraftModel):
1125 """Generic model for the Bombardier Dash 8-Q400 aircraft."""
1126 def __init__(self):
1127 """Construct the model."""
1128 super(DH8DModel, self). \
1129 __init__(flapsNotches = [0, 5, 10, 15, 35],
1130 fuelInfo = [0x0b74, 0x0b7c, 0xb94],
1131 numEngines = 2)
1132
1133 @property
1134 def name(self):
1135 """Get the name for this aircraft model."""
1136 return "FSUIPC/Generic Bombardier Dash 8-Q400"
1137
1138#------------------------------------------------------------------------------
1139
1140class DreamwingsDH8DModel(DH8DModel):
1141 """Model handler for the Dreamwings Dash 8-Q400."""
1142 @staticmethod
1143 def doesHandle(aircraft, (name, airPath)):
1144 """Determine if this model handler handles the aircraft with the given
1145 name."""
1146 return aircraft.type==const.AIRCRAFT_DH8D and \
1147 (name.find("Dreamwings")!=-1 or airPath.find("Dreamwings")!=-1) and \
1148 (name.find("Dash")!=-1 or airPath.find("Dash")!=-1) and \
1149 (name.find("Q400")!=-1 or airPath.find("Q400")!=-1) and \
1150 airPath.find("Dash8Q400")!=-1
1151
1152 @property
1153 def name(self):
1154 """Get the name for this aircraft model."""
1155 return "FSUIPC/Dreamwings Bombardier Dash 8-Q400"
1156
1157 def getAircraftState(self, aircraft, timestamp, data):
1158 """Get the aircraft state.
1159
1160 Get it from the parent, and then invert the pitot heat state."""
1161 state = super(DreamwingsDH8DModel, self).getAircraftState(aircraft,
1162 timestamp,
1163 data)
1164 state.pitotHeatOn = not state.pitotHeatOn
1165
1166 return state
1167#------------------------------------------------------------------------------
1168
1169class CRJ2Model(GenericAircraftModel):
1170 """Generic model for the Bombardier CRJ-200 aircraft."""
1171 def __init__(self):
1172 """Construct the model."""
1173 super(CRJ2Model, self). \
1174 __init__(flapsNotches = [0, 8, 20, 30, 45],
1175 fuelInfo = [0x0b74, 0x0b7c, 0xb94],
1176 numEngines = 2)
1177
1178 @property
1179 def name(self):
1180 """Get the name for this aircraft model."""
1181 return "FSUIPC/Generic Bombardier CRJ-200"
1182
1183#------------------------------------------------------------------------------
1184
1185class F70Model(GenericAircraftModel):
1186 """Generic model for the Fokker F70 aircraft."""
1187 def __init__(self):
1188 """Construct the model."""
1189 super(F70Model, self). \
1190 __init__(flapsNotches = [0, 8, 15, 25, 42],
1191 fuelInfo = [0x0b74, 0x0b7c, 0xb94],
1192 numEngines = 2)
1193
1194 @property
1195 def name(self):
1196 """Get the name for this aircraft model."""
1197 return "FSUIPC/Generic Fokker 70"
1198
1199#------------------------------------------------------------------------------
1200
1201class DC3Model(GenericAircraftModel):
1202 """Generic model for the Lisunov Li-2 (DC-3) aircraft."""
1203 def __init__(self):
1204 """Construct the model."""
1205 super(DC3Model, self). \
1206 __init__(flapsNotches = [0, 15, 30, 45],
1207 fuelInfo = [0x0b7c, 0x0b84, 0x0b94, 0x0b9c],
1208 numEngines = 2)
1209
1210 @property
1211 def name(self):
1212 """Get the name for this aircraft model."""
1213 return "FSUIPC/Generic Lisunov Li-2"
1214
1215#------------------------------------------------------------------------------
1216
1217class T134Model(GenericAircraftModel):
1218 """Generic model for the Tupolev Tu-134 aircraft."""
1219 def __init__(self):
1220 """Construct the model."""
1221 super(T134Model, self). \
1222 __init__(flapsNotches = [0, 10, 20, 30],
1223 fuelInfo = [0x0b74,
1224 0x0b8c, 0x0b84,
1225 0x0ba4, 0x0b9c,
1226 0x1254, 0x125c],
1227 numEngines = 2)
1228
1229 @property
1230 def name(self):
1231 """Get the name for this aircraft model."""
1232 return "FSUIPC/Generic Tupolev Tu-134"
1233
1234#------------------------------------------------------------------------------
1235
1236class T154Model(GenericAircraftModel):
1237 """Generic model for the Tupolev Tu-134 aircraft."""
1238 def __init__(self):
1239 """Construct the model."""
1240 super(T154Model, self). \
1241 __init__(flapsNotches = [0, 15, 28, 45],
1242 fuelInfo = [0x0b74, 0x0b7c, 0x0b94,
1243 0x1244, 0x0b84, 0x0b9c],
1244 numEngines = 3)
1245
1246 @property
1247 def name(self):
1248 """Get the name for this aircraft model."""
1249 return "FSUIPC/Generic Tupolev Tu-154"
1250
1251 def getAircraftState(self, aircraft, timestamp, data):
1252 """Get an aircraft state object for the given monitoring data.
1253
1254 This removes the reverser value for the middle engine."""
1255 state = super(T154Model, self).getAircraftState(aircraft, timestamp, data)
1256 del state.reverser[1]
1257 return state
1258
1259#------------------------------------------------------------------------------
1260
1261class YK40Model(GenericAircraftModel):
1262 """Generic model for the Yakovlev Yak-40 aircraft."""
1263 def __init__(self):
1264 """Construct the model."""
1265 super(YK40Model, self). \
1266 __init__(flapsNotches = [0, 20, 35],
1267 fuelInfo = [0x0b7c, 0x0b94],
1268 numEngines = 2)
1269
1270 @property
1271 def name(self):
1272 """Get the name for this aircraft model."""
1273 return "FSUIPC/Generic Yakovlev Yak-40"
1274
1275#------------------------------------------------------------------------------
1276
1277_genericModels = { const.AIRCRAFT_B736 : B737Model,
1278 const.AIRCRAFT_B737 : B737Model,
1279 const.AIRCRAFT_B738 : B737Model,
1280 const.AIRCRAFT_B733 : B737Model,
1281 const.AIRCRAFT_B734 : B737Model,
1282 const.AIRCRAFT_B735 : B737Model,
1283 const.AIRCRAFT_DH8D : DH8DModel,
1284 const.AIRCRAFT_B762 : B767Model,
1285 const.AIRCRAFT_B763 : B767Model,
1286 const.AIRCRAFT_CRJ2 : B767Model,
1287 const.AIRCRAFT_F70 : F70Model,
1288 const.AIRCRAFT_DC3 : DC3Model,
1289 const.AIRCRAFT_T134 : T134Model,
1290 const.AIRCRAFT_T154 : T154Model,
1291 const.AIRCRAFT_YK40 : YK40Model }
1292
1293#------------------------------------------------------------------------------
1294
1295AircraftModel.registerSpecial(PMDGBoeing737NGModel)
1296AircraftModel.registerSpecial(DreamwingsDH8DModel)
1297
1298#------------------------------------------------------------------------------
Note: See TracBrowser for help on using the repository browser.