source: src/mlx/fsuipc.py@ 135:940d8d54f6d6

Last change on this file since 135:940d8d54f6d6 was 134:9ce031d5d4a9, checked in by István Váradi <ivaradi@…>, 13 years ago

Most of the remaining messages are implemented

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