source: src/mlx/checks.py@ 329:cb7f4cf4e870

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

The ADF frequencies are also logged in a forced way at certain flight stages (#133)

File size: 52.5 KB
Line 
1
2import fs
3import const
4import util
5from acars import ACARS
6from sound import startSound
7
8import time
9
10#---------------------------------------------------------------------------------------
11
12## @package mlx.checks
13#
14# The classes that check the state of the aircraft.
15#
16# During the flight the program periodically queries various data from the
17# simulator. This data is returned in instances of the \ref
18# mlx.fs.AircraftState class and passed to the various "checkers",
19# i.e. instances of subclasses of the \ref StateChecker class. These checkers
20# perform various checks to see if the aircraft's parameters are within the
21# expected limits, or some of them just logs something.
22#
23# There are a few special ones, such as \ref StageChecker which computes the
24# transitions from one stage of the flight to the next one. Or \ref ACARSSender
25# which sends the ACARS periodically
26
27#---------------------------------------------------------------------------------------
28
29class StateChecker(object):
30 """Base class for classes the instances of which check the aircraft's state
31 from some aspect.
32
33 As a result of the check they may log something, or notify of some fault, etc."""
34 def check(self, flight, aircraft, logger, oldState, state):
35 """Perform the check and do whatever is needed.
36
37 This default implementation raises a NotImplementedError."""
38 raise NotImplementedError()
39
40#---------------------------------------------------------------------------------------
41
42class StageChecker(StateChecker):
43 """Check the flight stage transitions."""
44 def __init__(self):
45 """Construct the stage checker."""
46 self._flareStarted = False
47
48 def check(self, flight, aircraft, logger, oldState, state):
49 """Check the stage of the aircraft."""
50 stage = flight.stage
51 if stage==None:
52 aircraft.setStage(state, const.STAGE_BOARDING)
53 elif stage==const.STAGE_BOARDING:
54 if not state.parking or \
55 (not state.trickMode and state.groundSpeed>5.0):
56 aircraft.setStage(state, const.STAGE_PUSHANDTAXI)
57 elif stage==const.STAGE_PUSHANDTAXI or stage==const.STAGE_RTO:
58 if state.landingLightsOn or state.strobeLightsOn or \
59 state.groundSpeed>80.0:
60 aircraft.setStage(state, const.STAGE_TAKEOFF)
61 elif stage==const.STAGE_TAKEOFF:
62 if not state.gearsDown or \
63 (state.radioAltitude>3000.0 and state.vs>0):
64 aircraft.setStage(state, const.STAGE_CLIMB)
65 elif not state.landingLightsOn and \
66 not state.strobeLightsOn and \
67 state.onTheGround and \
68 state.groundSpeed<50.0:
69 aircraft.setStage(state, const.STAGE_RTO)
70 elif stage==const.STAGE_CLIMB:
71 if (state.altitude+2000) > flight.cruiseAltitude:
72 aircraft.setStage(state, const.STAGE_CRUISE)
73 elif state.radioAltitude<2000.0 and \
74 state.vs < 0.0 and state.gearsDown:
75 aircraft.setStage(state, const.STAGE_LANDING)
76 elif stage==const.STAGE_CRUISE:
77 if (state.altitude+2000) < flight.cruiseAltitude:
78 aircraft.setStage(state, const.STAGE_DESCENT)
79 elif stage==const.STAGE_DESCENT or stage==const.STAGE_GOAROUND:
80 if state.gearsDown and state.radioAltitude<2000.0:
81 aircraft.setStage(state, const.STAGE_LANDING)
82 elif (state.altitude+2000) > flight.cruiseAltitude:
83 aircraft.setStage(state, const.STAGE_CRUISE)
84 elif stage==const.STAGE_LANDING:
85 if state.onTheGround and state.groundSpeed<50.0:
86 aircraft.setStage(state, const.STAGE_TAXIAFTERLAND)
87 elif not state.gearsDown:
88 aircraft.setStage(state, const.STAGE_GOAROUND)
89 elif state.radioAltitude>200 and self._flareStarted:
90 aircraft.cancelFlare()
91 self._flareStarted = False
92 elif state.radioAltitude<150 and not state.onTheGround and \
93 not self._flareStarted:
94 self._flareStarted = True
95 aircraft.prepareFlare()
96 elif stage==const.STAGE_TAXIAFTERLAND:
97 if state.parking:
98 aircraft.setStage(state, const.STAGE_PARKING)
99 elif stage==const.STAGE_PARKING:
100 if aircraft.checkFlightEnd(state):
101 aircraft.setStage(state, const.STAGE_END)
102
103#---------------------------------------------------------------------------------------
104
105class ACARSSender(StateChecker):
106 """Sender of online ACARS.
107
108 It sends the ACARS every 3 minutes to the MAVA website."""
109
110 ## The interval at which the ACARS are sent
111 INTERVAL = 3*60.0
112
113 def __init__(self, gui):
114 """Construct the ACARS sender."""
115 self._gui = gui
116 self._lastSent = None
117
118 def check(self, flight, aircraft, logger, oldState, state):
119 """If the time has come to send the ACARS, send it."""
120 now = time.time()
121
122 if self._lastSent is not None and \
123 (self._lastSent + ACARSSender.INTERVAL)>now:
124 return
125
126 acars = ACARS(self._gui, state)
127 self._gui.webHandler.sendACARS(self._acarsCallback, acars)
128
129 def _acarsCallback(self, returned, result):
130 """Callback for ACARS sending."""
131 if returned:
132 print "Sent online ACARS"
133 self._lastSent = time.time() if self._lastSent is None \
134 else self._lastSent + ACARSSender.INTERVAL
135 else:
136 print "Failed to send the ACARS"
137
138#---------------------------------------------------------------------------------------
139
140class TakeOffLogger(StateChecker):
141 """Logger for the cruise speed."""
142 def __init__(self):
143 """Construct the logger."""
144 self._onTheGround = True
145
146 def check(self, flight, aircraft, logger, oldState, state):
147 """Log the cruise speed if necessary."""
148 if flight.stage==const.STAGE_TAKEOFF and \
149 self._onTheGround and not state.onTheGround:
150 logger.message(state.timestamp,
151 "Takeoff speed: %.0f %s" % \
152 (flight.speedFromKnots(state.ias),
153 flight.getEnglishSpeedUnit()))
154 logger.message(state.timestamp,
155 "Takeoff heading: %03.0f degrees" % (state.heading,))
156 logger.message(state.timestamp,
157 "Takeoff pitch: %.1f degrees" % (state.pitch,))
158 logger.message(state.timestamp,
159 "Centre of gravity: %.1f%%" % (state.cog*100.0,))
160 self._onTheGround = False
161
162#---------------------------------------------------------------------------------------
163
164class CruiseSpeedLogger(StateChecker):
165 """Logger for the cruise speed."""
166 def __init__(self):
167 """Construct the logger."""
168 self._lastTime = None
169
170 def check(self, flight, aircraft, logger, oldState, state):
171 """Log the cruise speed if necessary."""
172 if flight.stage==const.STAGE_CRUISE and \
173 (self._lastTime is None or \
174 (self._lastTime+800)<=state.timestamp):
175 if state.altitude>24500.0:
176 logger.message(state.timestamp,
177 "Cruise speed: %.3f mach" % (state.mach,))
178 else:
179 logger.message(state.timestamp,
180 "Cruise speed: %.0f %s" %
181 (flight.speedFromKnots(state.ias),
182 flight.getEnglishSpeedUnit()))
183 self._lastTime = state.timestamp
184
185#---------------------------------------------------------------------------------------
186
187class SpoilerLogger(StateChecker):
188 """Logger for the cruise speed."""
189 def __init__(self):
190 """Construct the logger."""
191 self._logged = False
192 self._spoilersExtension = None
193
194 def check(self, flight, aircraft, logger, oldState, state):
195 """Log the cruise speed if necessary."""
196 if flight.stage==const.STAGE_LANDING and not self._logged:
197 if state.onTheGround:
198 if state.spoilersExtension!=self._spoilersExtension:
199 logger.message(state.timestamp, "Spoilers deployed")
200 self._logged = True
201 config = flight.config
202 if config.enableSounds and config.speedbrakeAtTD:
203 startSound(const.SOUND_SPEEDBRAKE)
204 else:
205 self._spoilersExtension = state.spoilersExtension
206
207#---------------------------------------------------------------------------------------
208
209class VisibilityChecker(StateChecker):
210 """Inform the pilot of the visibility once when descending below 2000 ft,
211 then when descending below 1000 ft."""
212 def __init__(self):
213 """Construct the visibility checker."""
214 self._informedBelow2000 = False
215 self._informedBelow1000 = False
216
217 def check(self, flight, aircraft, logger, oldState, state):
218 """Check if we need to inform the pilot of the visibility."""
219 if flight.stage==const.STAGE_DESCENT or \
220 flight.stage==const.STAGE_LANDING:
221 if (state.radioAltitude<2000 and not self._informedBelow2000) or \
222 (state.radioAltitude<1000 and not self._informedBelow1000):
223 visibilityString = util.visibility2String(state.visibility)
224 fs.sendMessage(const.MESSAGETYPE_VISIBILITY,
225 "Current visibility: " + visibilityString,
226 5)
227 logger.message(state.timestamp,
228 "Pilot was informed about the visibility: " +
229 visibilityString)
230 self._informedBelow2000 = True
231 self._informedBelow1000 = state.radioAltitude<1000
232
233#---------------------------------------------------------------------------------------
234
235class ApproachCalloutsPlayer(StateChecker):
236 """A state checker that plays a sequence of approach callouts.
237
238 It tracks the altitude during the descent and landing phases and
239 if the altitude crosses one that has a callout associated with and
240 the vertical speed is negative, that callout will be played."""
241 def __init__(self, approachCallouts):
242 """Construct the approach callouts player."""
243 self._approachCallouts = approachCallouts
244 self._altitudes = approachCallouts.getAltitudes(descending = False)
245
246 def check(self, flight, aircraft, logger, oldState, state):
247 """Check if we need to play a callout."""
248 if (flight.stage==const.STAGE_DESCENT or \
249 flight.stage==const.STAGE_LANDING) and state.vs<0:
250 oldRadioAltitude = oldState.radioAltitude
251 radioAltitude = state.radioAltitude
252 for altitude in self._altitudes:
253 if radioAltitude<=altitude and \
254 oldRadioAltitude>altitude:
255 startSound(self._approachCallouts[altitude])
256 break
257
258#---------------------------------------------------------------------------------------
259
260class StateChangeLogger(StateChecker):
261 """Base class for classes the instances of which check if a specific change has
262 occured in the aircraft's state, and log such change."""
263 def __init__(self, logInitial = True):
264 """Construct the logger.
265
266 If logInitial is True, the initial value will be logged, not just the
267 changes later.
268
269 Child classes should define the following functions:
270 - _changed(self, oldState, state): returns a boolean indicating if the
271 value has changed or not
272 - _getMessage(self, flight, state, forced): return a strings containing
273 the message to log with the new value
274 """
275 self._logInitial = logInitial
276
277 def _getLogTimestamp(self, state, forced):
278 """Get the log timestamp."""
279 return state.timestamp
280
281 def check(self, flight, aircraft, logger, oldState, state):
282 """Check if the state has changed, and if so, log the new state."""
283 shouldLog = False
284 if oldState is None:
285 shouldLog = self._logInitial
286 else:
287 shouldLog = self._changed(oldState, state)
288
289 if shouldLog:
290 self.logState(flight, logger, state)
291
292 def logState(self, flight, logger, state, forced = False):
293 """Log the state."""
294 message = self._getMessage(flight, state, forced)
295 if message is not None:
296 logger.message(self._getLogTimestamp(state, forced), message)
297
298#-------------------------------------------------------------------------------
299
300class SimpleChangeMixin(object):
301 """A mixin that defines a _changed() function which simply calls a function
302 to retrieve the value two monitor for both states and compares them.
303
304 Child classes should define the following function:
305 - _getValue(state): get the value we are interested in."""
306 def _changed(self, oldState, state):
307 """Determine if the value has changed."""
308 currentValue = self._getValue(state)
309 return currentValue is not None and self._getValue(oldState)!=currentValue
310
311#---------------------------------------------------------------------------------------
312
313class SingleValueMixin(object):
314 """A mixin that provides a _getValue() function to query a value from the
315 state using the name of the attribute."""
316 def __init__(self, attrName):
317 """Construct the mixin with the given attribute name."""
318 self._attrName = attrName
319
320 def _getValue(self, state):
321 """Get the value of the attribute from the state."""
322 return getattr(state, self._attrName)
323
324#---------------------------------------------------------------------------------------
325
326class DelayedChangeMixin(object):
327 """A mixin to a StateChangeLogger that stores the old value and reports a
328 change only if the change has been there for a certain amount of time.
329
330 Child classes should define the following function:
331 - _getValue(state): get the value we are interested in."""
332 def __init__(self, minDelay = 3.0, maxDelay = 10.0):
333 """Construct the mixin with the given delay in seconds."""
334 self._minDelay = minDelay
335 self._maxDelay = maxDelay
336 self._oldValue = None
337 self._firstChange = None
338 self._lastChangeState = None
339
340 def _changed(self, oldState, state):
341 """Determine if the value has changed."""
342 if self._oldValue is None:
343 self._oldValue = self._getValue(oldState)
344
345 newValue = self._getValue(state)
346 if self._isDifferent(self._oldValue, newValue):
347 if self._firstChange is None:
348 self._firstChange = state.timestamp
349 self._lastChangeState = state
350 self._oldValue = newValue
351
352 if self._firstChange is not None:
353 if state.timestamp >= min(self._lastChangeState.timestamp +
354 self._minDelay,
355 self._firstChange + self._maxDelay):
356 self._firstChange = None
357 return True
358
359 return False
360
361 def _getLogTimestamp(self, state, forced):
362 """Get the log timestamp."""
363 return self._lastChangeState.timestamp \
364 if not forced and self._lastChangeState is not None \
365 else state.timestamp
366
367 def _isDifferent(self, oldValue, newValue):
368 """Determine if the given values are different.
369
370 This default implementation checks for simple equality."""
371 return oldValue!=newValue
372
373#---------------------------------------------------------------------------------------
374
375class TemplateMessageMixin(object):
376 """Mixin to generate a message based on a template.
377
378 Child classes should define the following function:
379 - _getValue(state): get the value we are interested in."""
380 def __init__(self, template):
381 """Construct the mixin."""
382 self._template = template
383
384 def _getMessage(self, flight, state, forced):
385 """Get the message."""
386 value = self._getValue(state)
387 return None if value is None else self._template % (value,)
388
389#---------------------------------------------------------------------------------------
390
391class GenericStateChangeLogger(StateChangeLogger, SingleValueMixin,
392 DelayedChangeMixin, TemplateMessageMixin):
393 """Base for generic state change loggers that monitor a single value in the
394 state possibly with a delay and the logged message comes from a template"""
395 def __init__(self, attrName, template, logInitial = True,
396 minDelay = 0.0, maxDelay = 0.0):
397 """Construct the object."""
398 StateChangeLogger.__init__(self, logInitial = logInitial)
399 SingleValueMixin.__init__(self, attrName)
400 DelayedChangeMixin.__init__(self, minDelay = minDelay,
401 maxDelay = maxDelay)
402 TemplateMessageMixin.__init__(self, template)
403 self._getLogTimestamp = \
404 lambda state, forced: \
405 DelayedChangeMixin._getLogTimestamp(self, state, forced)
406
407#---------------------------------------------------------------------------------------
408
409class ForceableLoggerMixin(object):
410 """A mixin for loggers that can be forced to log a certain state.
411
412 The last logged state is always maintained, and when checking for a change,
413 that state is compared to the current one (which may actually be the same,
414 if a forced logging was performed for that state).
415
416 Children should implement the following functions:
417 - _hasChanged(oldState, state): the real check for a change
418 - _logState(flight, logger, state, forced): the real logging function
419 """
420 def __init__(self):
421 """Construct the mixin."""
422 self._lastLoggedState = None
423
424 def forceLog(self, flight, logger, state):
425 """Force logging the given state."""
426 self.logState(flight, logger, state, forced = True)
427
428 def logState(self, flight, logger, state, forced = False):
429 """Log the state.
430
431 It calls _logState to perform the real logging, and saves the given
432 state as the last logged one."""
433 self._logState(flight, logger, state, forced)
434 self._lastLoggedState = state
435
436 def _changed(self, oldState, state):
437 """Check if the state has changed.
438
439 This function calls _hasChanged for the real check, and replaces
440 oldState with the stored last logged state, if any."""
441 if self._lastLoggedState is not None:
442 oldState = self._lastLoggedState
443 return self._hasChanged(oldState, state)
444
445#---------------------------------------------------------------------------------------
446
447class AltimeterLogger(StateChangeLogger, SingleValueMixin,
448 DelayedChangeMixin):
449 """Logger for the altimeter setting."""
450 def __init__(self):
451 """Construct the logger."""
452 StateChangeLogger.__init__(self, logInitial = True)
453 SingleValueMixin.__init__(self, "altimeter")
454 DelayedChangeMixin.__init__(self)
455 self._getLogTimestamp = \
456 lambda state, forced: \
457 DelayedChangeMixin._getLogTimestamp(self, state, forced)
458
459 def _getMessage(self, flight, state, forced):
460 """Get the message to log on a change."""
461 logState = self._lastChangeState if \
462 self._lastChangeState is not None else state
463 return "Altimeter: %.0f hPa at %.0f feet" % \
464 (logState.altimeter, logState.altitude)
465
466#---------------------------------------------------------------------------------------
467
468class NAVLogger(StateChangeLogger, DelayedChangeMixin, ForceableLoggerMixin):
469 """Logger for NAV radios.
470
471 It also logs the OBS frequency set."""
472 @staticmethod
473 def getMessage(logName, frequency, obs):
474 """Get the message for the given NAV radio setting."""
475 message = u"%s frequency: %s MHz" % (logName, frequency)
476 if obs is not None: message += u" (%d\u00b0)" % (obs,)
477 return message
478
479 def __init__(self, attrName, logName):
480 """Construct the NAV logger."""
481 StateChangeLogger.__init__(self, logInitial = True)
482 DelayedChangeMixin.__init__(self)
483 ForceableLoggerMixin.__init__(self)
484
485 self.logState = lambda flight, logger, state, forced = False: \
486 ForceableLoggerMixin.logState(self, flight, logger, state,
487 forced = forced)
488 self._getLogTimestamp = \
489 lambda state, forced: \
490 DelayedChangeMixin._getLogTimestamp(self, state, forced)
491 self._changed = lambda oldState, state: \
492 ForceableLoggerMixin._changed(self, oldState, state)
493 self._hasChanged = lambda oldState, state: \
494 DelayedChangeMixin._changed(self, oldState, state)
495 self._logState = lambda flight, logger, state, forced: \
496 StateChangeLogger.logState(self, flight, logger, state,
497 forced = forced)
498
499 self._attrName = attrName
500 self._logName = logName
501
502 def _getValue(self, state):
503 """Get the value.
504
505 If both the frequency and the obs settings are available, a tuple
506 containing them is returned, otherwise None."""
507 frequency = getattr(state, self._attrName)
508 obs = getattr(state, self._attrName + "_obs")
509 manual = getattr(state, self._attrName + "_manual")
510 return (frequency, obs, manual)
511
512 def _getMessage(self, flight, state, forced):
513 """Get the message."""
514 (frequency, obs, manual) = self._getValue(state)
515 return None if frequency is None or obs is None or \
516 (not manual and not forced) else \
517 self.getMessage(self._logName, frequency, obs)
518
519 def _isDifferent(self, oldValue, newValue):
520 """Determine if the valie has changed between the given states."""
521 (oldFrequency, oldOBS, _oldManual) = oldValue
522 (newFrequency, newOBS, _newManual) = newValue
523 return oldFrequency!=newFrequency or oldOBS!=newOBS
524
525#---------------------------------------------------------------------------------------
526
527class NAV1Logger(NAVLogger):
528 """Logger for the NAV1 radio setting."""
529 def __init__(self):
530 """Construct the logger."""
531 super(NAV1Logger, self).__init__("nav1", "NAV1")
532
533#---------------------------------------------------------------------------------------
534
535class NAV2Logger(NAVLogger):
536 """Logger for the NAV2 radio setting."""
537 def __init__(self):
538 """Construct the logger."""
539 super(NAV2Logger, self).__init__("nav2", "NAV2")
540
541#---------------------------------------------------------------------------------------
542
543class ADFLogger(GenericStateChangeLogger, ForceableLoggerMixin):
544 """Base class for the ADF loggers."""
545 def __init__(self, attr, logName):
546 """Construct the ADF logger."""
547 GenericStateChangeLogger.__init__(self, attr,
548 "%s frequency: %%s kHz" % (logName,),
549 minDelay = 3.0, maxDelay = 10.0)
550 ForceableLoggerMixin.__init__(self)
551
552 self.logState = lambda flight, logger, state, forced = False: \
553 ForceableLoggerMixin.logState(self, flight, logger, state,
554 forced = forced)
555 self._changed = lambda oldState, state: \
556 ForceableLoggerMixin._changed(self, oldState, state)
557 self._hasChanged = lambda oldState, state: \
558 DelayedChangeMixin._changed(self, oldState, state)
559 self._logState = lambda flight, logger, state, forced: \
560 StateChangeLogger.logState(self, flight, logger, state, forced)
561
562#---------------------------------------------------------------------------------------
563
564class ADF1Logger(ADFLogger):
565 """Logger for the ADF1 radio setting."""
566 def __init__(self):
567 """Construct the logger."""
568 super(ADF1Logger, self).__init__("adf1", "ADF1")
569
570#---------------------------------------------------------------------------------------
571
572class ADF2Logger(ADFLogger):
573 """Logger for the ADF2 radio setting."""
574 def __init__(self):
575 """Construct the logger."""
576 super(ADF2Logger, self).__init__("adf2", "ADF2")
577
578#---------------------------------------------------------------------------------------
579
580class SquawkLogger(GenericStateChangeLogger):
581 """Logger for the squawk setting."""
582 def __init__(self):
583 """Construct the logger."""
584 super(SquawkLogger, self).__init__("squawk", "Squawk code: %s",
585 minDelay = 3.0, maxDelay = 10.0)
586
587#---------------------------------------------------------------------------------------
588
589class LightsLogger(StateChangeLogger, SingleValueMixin, SimpleChangeMixin):
590 """Base class for the loggers of the various lights."""
591 def __init__(self, attrName, template):
592 """Construct the logger."""
593 StateChangeLogger.__init__(self)
594 SingleValueMixin.__init__(self, attrName)
595
596 self._template = template
597
598 def _getMessage(self, flight, state, forced):
599 """Get the message from the given state."""
600 return self._template % ("ON" if self._getValue(state) else "OFF")
601
602#---------------------------------------------------------------------------------------
603
604class AnticollisionLightsLogger(LightsLogger):
605 """Logger for the anti-collision lights."""
606 def __init__(self):
607 LightsLogger.__init__(self, "antiCollisionLightsOn",
608 "Anti-collision lights: %s")
609
610#---------------------------------------------------------------------------------------
611
612class LandingLightsLogger(LightsLogger):
613 """Logger for the landing lights."""
614 def __init__(self):
615 LightsLogger.__init__(self, "landingLightsOn",
616 "Landing lights: %s")
617
618#---------------------------------------------------------------------------------------
619
620class StrobeLightsLogger(LightsLogger):
621 """Logger for the strobe lights."""
622 def __init__(self):
623 LightsLogger.__init__(self, "strobeLightsOn",
624 "Strobe lights: %s")
625
626#---------------------------------------------------------------------------------------
627
628class NavLightsLogger(LightsLogger):
629 """Logger for the navigational lights."""
630 def __init__(self):
631 LightsLogger.__init__(self, "navLightsOn",
632 "Navigational lights: %s")
633
634#---------------------------------------------------------------------------------------
635
636class FlapsLogger(StateChangeLogger, SingleValueMixin, SimpleChangeMixin):
637 """Logger for the flaps setting."""
638 def __init__(self):
639 """Construct the logger."""
640 StateChangeLogger.__init__(self, logInitial = True)
641 SingleValueMixin.__init__(self, "flapsSet")
642
643 def _getMessage(self, flight, state, forced):
644 """Get the message to log on a change."""
645 speed = state.groundSpeed if state.groundSpeed<80.0 else state.ias
646 return "Flaps set to %.0f at %.0f %s" % \
647 (state.flapsSet, flight.speedFromKnots(speed),
648 flight.getEnglishSpeedUnit())
649
650#---------------------------------------------------------------------------------------
651
652class GearsLogger(StateChangeLogger, SingleValueMixin, SimpleChangeMixin):
653 """Logger for the gears state."""
654 def __init__(self):
655 """Construct the logger."""
656 StateChangeLogger.__init__(self, logInitial = True)
657 SingleValueMixin.__init__(self, "gearControlDown")
658
659 def _getMessage(self, flight, state, forced):
660 """Get the message to log on a change."""
661 return "Gears SET to %s at %.0f %s, %.0f feet" % \
662 ("DOWN" if state.gearControlDown else "UP",
663 flight.speedFromKnots(state.ias),
664 flight.getEnglishSpeedUnit(), state.altitude)
665
666#---------------------------------------------------------------------------------------
667
668class FaultChecker(StateChecker):
669 """Base class for checkers that look for faults."""
670 @staticmethod
671 def _appendDuring(flight, message):
672 """Append a 'during XXX' test to the given message, depending on the
673 flight stage."""
674 stageStr = const.stage2string(flight.stage)
675 return message if stageStr is None \
676 else (message + " during " + stageStr.upper())
677
678 @staticmethod
679 def _getLinearScore(minFaultValue, maxFaultValue, minScore, maxScore,
680 value):
681 """Get the score for a faulty value where the score is calculated
682 linearly within a certain range."""
683 if value<minFaultValue:
684 return 0
685 elif value>maxFaultValue:
686 return maxScore
687 else:
688 return minScore + (maxScore-minScore) * (value-minFaultValue) / \
689 (maxFaultValue - minFaultValue)
690
691#---------------------------------------------------------------------------------------
692
693class SimpleFaultChecker(FaultChecker):
694 """Base class for fault checkers that check for a single occurence of a
695 faulty condition.
696
697 Child classes should implement the following functions:
698 - isCondition(self, flight, aircraft, oldState, state): should return whether the
699 condition holds
700 - logFault(self, flight, aircraft, logger, oldState, state): log the fault
701 via the logger."""
702 def check(self, flight, aircraft, logger, oldState, state):
703 """Perform the check."""
704 if self.isCondition(flight, aircraft, oldState, state):
705 self.logFault(flight, aircraft, logger, oldState, state)
706
707#---------------------------------------------------------------------------------------
708
709class PatientFaultChecker(FaultChecker):
710 """A fault checker that does not decides on a fault when the condition
711 arises immediately, but can wait some time.
712
713 Child classes should implement the following functions:
714 - isCondition(self, flight, aircraft, oldState, state): should return whether the
715 condition holds
716 - logFault(self, flight, aircraft, logger, oldState, state): log the fault
717 via the logger
718 """
719 def __init__(self, timeout = 2.0):
720 """Construct the fault checker with the given timeout."""
721 self._timeout = timeout
722 self._faultStarted = None
723
724 def getTimeout(self, flight, aircraft, oldState, state):
725 """Get the timeout.
726
727 This default implementation returns the timeout given in the
728 constructor, but child classes might want to enforce a different
729 policy."""
730 return self._timeout
731
732 def check(self, flight, aircraft, logger, oldState, state):
733 """Perform the check."""
734 if self.isCondition(flight, aircraft, oldState, state):
735 if self._faultStarted is None:
736 self._faultStarted = state.timestamp
737 timeout = self.getTimeout(flight, aircraft, oldState, state)
738 if state.timestamp>=(self._faultStarted + timeout):
739 self.logFault(flight, aircraft, logger, oldState, state)
740 self._faultStarted = state.timestamp
741 else:
742 self._faultStarted = None
743
744#---------------------------------------------------------------------------------------
745
746class AntiCollisionLightsChecker(PatientFaultChecker):
747 """Check for the anti-collision light being off at high N1 values."""
748 def isCondition(self, flight, aircraft, oldState, state):
749 """Check if the fault condition holds."""
750 return (flight.stage!=const.STAGE_PARKING or \
751 not flight.config.usingFS2Crew) and \
752 not state.antiCollisionLightsOn and \
753 self.isEngineCondition(state)
754
755 def logFault(self, flight, aircraft, logger, oldState, state):
756 """Log the fault."""
757 flight.handleFault(AntiCollisionLightsChecker, state.timestamp,
758 FaultChecker._appendDuring(flight,
759 "Anti-collision lights were off"),
760 1)
761
762 def isEngineCondition(self, state):
763 """Determine if the engines are in such a state that the lights should
764 be on."""
765 if state.n1 is not None:
766 return max(state.n1)>5
767 elif state.rpm is not None:
768 return max(state.rpm)>0
769 else:
770 return False
771
772#---------------------------------------------------------------------------------------
773
774class TupolevAntiCollisionLightsChecker(AntiCollisionLightsChecker):
775 """Check for the anti-collision light for Tuplev planes."""
776 def isCondition(self, flight, aircraft, oldState, state):
777 """Check if the fault condition holds."""
778 numEnginesRunning = 0
779 for n1 in state.n1:
780 if n1>5: numEnginesRunning += 1
781
782 if flight.stage==const.STAGE_PARKING:
783 return numEnginesRunning<len(state.n1) \
784 and state.antiCollisionLightsOn
785 else:
786 return numEnginesRunning>1 and not state.antiCollisionLightsOn or \
787 numEnginesRunning<1 and state.antiCollisionLightsOn
788
789#---------------------------------------------------------------------------------------
790
791class BankChecker(SimpleFaultChecker):
792 """Check for the bank is within limits."""
793 def isCondition(self, flight, aircraft, oldState, state):
794 """Check if the fault condition holds."""
795 if flight.stage==const.STAGE_CRUISE:
796 bankLimit = 30
797 elif flight.stage in [const.STAGE_TAKEOFF, const.STAGE_CLIMB,
798 const.STAGE_DESCENT, const.STAGE_LANDING]:
799 bankLimit = 35
800 else:
801 return False
802
803 return state.bank>bankLimit or state.bank<-bankLimit
804
805 def logFault(self, flight, aircraft, logger, oldState, state):
806 """Log the fault."""
807 flight.handleFault(BankChecker, state.timestamp,
808 FaultChecker._appendDuring(flight, "Bank too steep"),
809 2)
810
811#---------------------------------------------------------------------------------------
812
813class FlapsRetractChecker(SimpleFaultChecker):
814 """Check if the flaps are not retracted too early."""
815 def __init__(self):
816 """Construct the flaps checker."""
817 self._timeStart = None
818
819 def isCondition(self, flight, aircraft, oldState, state):
820 """Check if the fault condition holds.
821
822 FIXME: check if this really is the intention (FlapsRetractedMistake.java)"""
823 if (flight.stage==const.STAGE_TAKEOFF and not state.onTheGround) or \
824 (flight.stage==const.STAGE_LANDING and state.onTheGround):
825 if self._timeStart is None:
826 self._timeStart = state.timestamp
827
828 if state.flapsSet==0 and state.timestamp<=(self._timeStart+2.0):
829 return True
830 else:
831 self._timeStart = None
832 return False
833
834 def logFault(self, flight, aircraft, logger, oldState, state):
835 """Log the fault."""
836 flight.handleFault(FlapsRetractChecker, state.timestamp,
837 FaultChecker._appendDuring(flight, "Flaps retracted"),
838 20)
839
840#---------------------------------------------------------------------------------------
841
842class FlapsSpeedLimitChecker(SimpleFaultChecker):
843 """Check if the flaps are extended only at the right speeds."""
844 def isCondition(self, flight, aircraft, oldState, state):
845 """Check if the fault condition holds."""
846 speedLimit = aircraft.getFlapsSpeedLimit(state.flapsSet)
847 return speedLimit is not None and state.smoothedIAS>speedLimit
848
849 def logFault(self, flight, aircraft, logger, oldState, state):
850 """Log the fault."""
851 flight.handleFault(FlapsSpeedLimitChecker, state.timestamp,
852 FaultChecker._appendDuring(flight, "Flap speed limit fault"),
853 5)
854
855#---------------------------------------------------------------------------------------
856
857class GearsDownChecker(SimpleFaultChecker):
858 """Check if the gears are down at low altitudes."""
859 def isCondition(self, flight, aircraft, oldState, state):
860 """Check if the fault condition holds."""
861 return state.radioAltitude<10 and not state.gearsDown and \
862 flight.stage!=const.STAGE_TAKEOFF
863
864 def logFault(self, flight, aircraft, logger, oldState, state):
865 """Log the fault."""
866 flight.handleNoGo(GearsDownChecker, state.timestamp,
867 "Gears not down at %.0f feet radio altitude" % \
868 (state.radioAltitude,),
869 "GEAR DOWN NO GO")
870
871#---------------------------------------------------------------------------------------
872
873class GearSpeedLimitChecker(PatientFaultChecker):
874 """Check if the gears not down at too high a speed."""
875 def isCondition(self, flight, aircraft, oldState, state):
876 """Check if the fault condition holds."""
877 return state.gearsDown and state.smoothedIAS>aircraft.gearSpeedLimit
878
879 def logFault(self, flight, aircraft, logger, oldState, state):
880 """Log the fault."""
881 flight.handleFault(GearSpeedLimitChecker, state.timestamp,
882 FaultChecker._appendDuring(flight, "Gear speed limit fault"),
883 5)
884
885#---------------------------------------------------------------------------------------
886
887class GLoadChecker(SimpleFaultChecker):
888 """Check if the G-load does not exceed 2 except during flare."""
889 def isCondition(self, flight, aircraft, oldState, state):
890 """Check if the fault condition holds."""
891 return state.gLoad>2.0 and (flight.stage!=const.STAGE_LANDING or \
892 state.radioAltitude>=50)
893
894 def logFault(self, flight, aircraft, logger, oldState, state):
895 """Log the fault."""
896 flight.handleFault(GLoadChecker, state.timestamp,
897 "G-load was %.2f" % (state.gLoad,),
898 10)
899
900#---------------------------------------------------------------------------------------
901
902class LandingLightsChecker(PatientFaultChecker):
903 """Check if the landing lights are used properly."""
904 def getTimeout(self, flight, aircraft, oldState, state):
905 """Get the timeout.
906
907 It is the default timeout except for landing and takeoff."""
908 return 0.0 if flight.stage in [const.STAGE_TAKEOFF,
909 const.STAGE_LANDING] else self._timeout
910
911 def isCondition(self, flight, aircraft, oldState, state):
912 """Check if the fault condition holds."""
913 return state.landingLightsOn is not None and \
914 ((flight.stage==const.STAGE_BOARDING and \
915 state.landingLightsOn and state.onTheGround) or \
916 (flight.stage==const.STAGE_TAKEOFF and \
917 not state.landingLightsOn and not state.onTheGround) or \
918 (flight.stage in [const.STAGE_CLIMB, const.STAGE_CRUISE,
919 const.STAGE_DESCENT] and \
920 state.landingLightsOn and state.altitude>12500) or \
921 (flight.stage==const.STAGE_LANDING and \
922 not state.landingLightsOn and state.onTheGround) or \
923 (flight.stage==const.STAGE_PARKING and \
924 state.landingLightsOn and state.onTheGround))
925
926 def logFault(self, flight, aircraft, logger, oldState, state):
927 """Log the fault."""
928 score = 0 if flight.stage==const.STAGE_LANDING else 1
929 message = "Landing lights were %s" % (("on" if state.landingLightsOn else "off"),)
930 flight.handleFault(LandingLightsChecker, state.timestamp,
931 FaultChecker._appendDuring(flight, message),
932 score)
933
934#---------------------------------------------------------------------------------------
935
936class WeightChecker(PatientFaultChecker):
937 """Base class for checkers that check that some limit is not exceeded."""
938 def __init__(self, name):
939 """Construct the checker."""
940 super(WeightChecker, self).__init__(timeout = 5.0)
941 self._name = name
942
943 def isCondition(self, flight, aircraft, oldState, state):
944 """Check if the fault condition holds."""
945 if flight.entranceExam:
946 return False
947
948 limit = self.getLimit(flight, aircraft, state)
949 if limit is not None:
950 #if flight.options.compensation is not None:
951 # limit += flight.options.compensation
952 return self.getWeight(state)>limit
953
954 return False
955
956 def logFault(self, flight, aircraft, logger, oldState, state):
957 """Log the fault."""
958 mname = "M" + self._name
959 flight.handleNoGo(self.__class__, state.timestamp,
960 "%s exceeded: %s is %.0f kg" % \
961 (mname, self._name, self.getWeight(state)),
962 "%s NO GO" % (mname,))
963
964 def getWeight(self, state):
965 """Get the weight that is interesting for us."""
966 return state.grossWeight
967
968#---------------------------------------------------------------------------------------
969
970class MLWChecker(WeightChecker):
971 """Checks if the MLW is not exceeded on landing."""
972 def __init__(self):
973 """Construct the checker."""
974 super(MLWChecker, self).__init__("LW")
975
976 def getLimit(self, flight, aircraft, state):
977 """Get the limit if we are in the right state."""
978 return aircraft.mlw if flight.stage==const.STAGE_LANDING and \
979 state.onTheGround and \
980 not flight.entranceExam else None
981
982#---------------------------------------------------------------------------------------
983
984class MTOWChecker(WeightChecker):
985 """Checks if the MTOW is not exceeded on landing."""
986 def __init__(self):
987 """Construct the checker."""
988 super(MTOWChecker, self).__init__("TOW")
989
990 def getLimit(self, flight, aircraft, state):
991 """Get the limit if we are in the right state."""
992 return aircraft.mtow if flight.stage==const.STAGE_TAKEOFF and \
993 not flight.entranceExam else None
994
995#---------------------------------------------------------------------------------------
996
997class MZFWChecker(WeightChecker):
998 """Checks if the MZFW is not exceeded on landing."""
999 def __init__(self):
1000 """Construct the checker."""
1001 super(MZFWChecker, self).__init__("ZFW")
1002
1003 def getLimit(self, flight, aircraft, state):
1004 """Get the limit if we are in the right state."""
1005 return aircraft.mzfw if not flight.entranceExam else None
1006
1007 def getWeight(self, state):
1008 """Get the weight that is interesting for us."""
1009 return state.zfw
1010
1011#---------------------------------------------------------------------------------------
1012
1013class NavLightsChecker(PatientFaultChecker):
1014 """Check if the navigational lights are used properly."""
1015 def isCondition(self, flight, aircraft, oldState, state):
1016 """Check if the fault condition holds."""
1017 return flight.stage!=const.STAGE_BOARDING and \
1018 flight.stage!=const.STAGE_PARKING and \
1019 not state.navLightsOn
1020
1021 def logFault(self, flight, aircraft, logger, oldState, state):
1022 """Log the fault."""
1023 flight.handleFault(NavLightsChecker, state.timestamp,
1024 FaultChecker._appendDuring(flight,
1025 "Navigation lights were off"),
1026 1)
1027
1028#---------------------------------------------------------------------------------------
1029
1030class OverspeedChecker(PatientFaultChecker):
1031 """Check if Vne has been exceeded."""
1032 def __init__(self, timeout = 5.0):
1033 """Construct the checker."""
1034 super(OverspeedChecker, self).__init__(timeout = timeout)
1035
1036 def isCondition(self, flight, aircraft, oldState, state):
1037 """Check if the fault condition holds."""
1038 return state.overspeed
1039
1040 def logFault(self, flight, aircraft, logger, oldState, state):
1041 """Log the fault."""
1042 flight.handleFault(OverspeedChecker, state.timestamp,
1043 FaultChecker._appendDuring(flight, "Overspeed"),
1044 20)
1045
1046#---------------------------------------------------------------------------------------
1047
1048class PayloadChecker(SimpleFaultChecker):
1049 """Check if the payload matches the specification."""
1050 TOLERANCE=550
1051
1052 @staticmethod
1053 def isZFWFaulty(aircraftZFW, flightZFW):
1054 """Check if the given aircraft's ZFW is outside of the limits."""
1055 return aircraftZFW < (flightZFW - PayloadChecker.TOLERANCE) or \
1056 aircraftZFW > (flightZFW + PayloadChecker.TOLERANCE)
1057
1058 def isCondition(self, flight, aircraft, oldState, state):
1059 """Check if the fault condition holds."""
1060 return not flight.entranceExam and \
1061 flight.stage==const.STAGE_PUSHANDTAXI and \
1062 PayloadChecker.isZFWFaulty(state.zfw, flight.zfw)
1063
1064 def logFault(self, flight, aircraft, logger, oldState, state):
1065 """Log the fault."""
1066 flight.handleNoGo(PayloadChecker, state.timestamp,
1067 "ZFW difference is more than %d kgs" % \
1068 (PayloadChecker.TOLERANCE,),
1069 "ZFW NO GO")
1070
1071#---------------------------------------------------------------------------------------
1072
1073class PitotChecker(PatientFaultChecker):
1074 """Check if pitot heat is on."""
1075 def __init__(self):
1076 """Construct the checker."""
1077 super(PitotChecker, self).__init__(timeout = 3.0)
1078
1079 def isCondition(self, flight, aircraft, oldState, state):
1080 """Check if the fault condition holds."""
1081 return state.groundSpeed>80 and not state.pitotHeatOn
1082
1083 def logFault(self, flight, aircraft, logger, oldState, state):
1084 """Log the fault."""
1085 score = 2 if flight.stage in [const.STAGE_TAKEOFF, const.STAGE_CLIMB,
1086 const.STAGE_CRUISE, const.STAGE_DESCENT,
1087 const.STAGE_LANDING] else 0
1088 flight.handleFault(PitotChecker, state.timestamp,
1089 FaultChecker._appendDuring(flight, "Pitot heat was off"),
1090 score)
1091
1092#---------------------------------------------------------------------------------------
1093
1094class ReverserChecker(SimpleFaultChecker):
1095 """Check if the reverser is not used below 60 knots."""
1096 def isCondition(self, flight, aircraft, oldState, state):
1097 """Check if the fault condition holds."""
1098 return flight.stage in [const.STAGE_DESCENT, const.STAGE_LANDING,
1099 const.STAGE_TAXIAFTERLAND] and \
1100 state.groundSpeed<60 and max(state.reverser)
1101
1102 def logFault(self, flight, aircraft, logger, oldState, state):
1103 """Log the fault."""
1104 message = "Reverser used below %.0f %s" % \
1105 (flight.speedFromKnots(60), flight.getEnglishSpeedUnit())
1106 flight.handleFault(ReverserChecker, state.timestamp,
1107 FaultChecker._appendDuring(flight, message),
1108 15)
1109
1110#---------------------------------------------------------------------------------------
1111
1112class SpeedChecker(SimpleFaultChecker):
1113 """Check if the speed is in the prescribed limits."""
1114 def isCondition(self, flight, aircraft, oldState, state):
1115 """Check if the fault condition holds."""
1116 return flight.stage in [const.STAGE_PUSHANDTAXI,
1117 const.STAGE_TAXIAFTERLAND] and \
1118 state.groundSpeed>50
1119
1120 def logFault(self, flight, aircraft, logger, oldState, state):
1121 """Log the fault."""
1122 message = "Taxi speed over %.0f %s" % \
1123 (flight.speedFromKnots(50), flight.getEnglishSpeedUnit())
1124 flight.handleFault(SpeedChecker, state.timestamp,
1125 FaultChecker._appendDuring(flight, message),
1126 FaultChecker._getLinearScore(50, 80, 10, 15,
1127 state.groundSpeed))
1128
1129#---------------------------------------------------------------------------------------
1130
1131class StallChecker(PatientFaultChecker):
1132 """Check if stall occured."""
1133 def isCondition(self, flight, aircraft, oldState, state):
1134 """Check if the fault condition holds."""
1135 return flight.stage in [const.STAGE_TAKEOFF, const.STAGE_CLIMB,
1136 const.STAGE_CRUISE, const.STAGE_DESCENT,
1137 const.STAGE_LANDING] and state.stalled
1138
1139 def logFault(self, flight, aircraft, logger, oldState, state):
1140 """Log the fault."""
1141 score = 40 if flight.stage in [const.STAGE_TAKEOFF,
1142 const.STAGE_LANDING] else 30
1143 flight.handleFault(StallChecker, state.timestamp,
1144 FaultChecker._appendDuring(flight, "Stalled"),
1145 score)
1146
1147#---------------------------------------------------------------------------------------
1148
1149class StrobeLightsChecker(PatientFaultChecker):
1150 """Check if the strobe lights are used properly."""
1151 def isCondition(self, flight, aircraft, oldState, state):
1152 """Check if the fault condition holds."""
1153 return (flight.stage==const.STAGE_BOARDING and \
1154 state.strobeLightsOn and state.onTheGround) or \
1155 (flight.stage==const.STAGE_TAKEOFF and \
1156 not state.strobeLightsOn and not state.gearsDown) or \
1157 (flight.stage in [const.STAGE_CLIMB, const.STAGE_CRUISE,
1158 const.STAGE_DESCENT] and \
1159 not state.strobeLightsOn and not state.onTheGround) or \
1160 (flight.stage==const.STAGE_PARKING and \
1161 state.strobeLightsOn and state.onTheGround)
1162
1163 def logFault(self, flight, aircraft, logger, oldState, state):
1164 """Log the fault."""
1165 message = "Strobe lights were %s" % (("on" if state.strobeLightsOn else "off"),)
1166 flight.handleFault(StrobeLightsChecker, state.timestamp,
1167 FaultChecker._appendDuring(flight, message),
1168 1)
1169
1170#---------------------------------------------------------------------------------------
1171
1172class ThrustChecker(SimpleFaultChecker):
1173 """Check if the thrust setting is not too high during takeoff.
1174
1175 FIXME: is this really so general, for all aircraft?"""
1176 def isCondition(self, flight, aircraft, oldState, state):
1177 """Check if the fault condition holds."""
1178 return flight.stage==const.STAGE_TAKEOFF and \
1179 state.n1 is not None and max(state.n1)>97
1180
1181 def logFault(self, flight, aircraft, logger, oldState, state):
1182 """Log the fault."""
1183 flight.handleFault(ThrustChecker, state.timestamp,
1184 FaultChecker._appendDuring(flight,
1185 "Thrust setting was too high (>97%)"),
1186 FaultChecker._getLinearScore(97, 110, 0, 10, max(state.n1)))
1187
1188#---------------------------------------------------------------------------------------
1189
1190class VSChecker(SimpleFaultChecker):
1191 """Check if the vertical speed is not too low at certain altitudes"""
1192 BELOW10000 = -5000
1193 BELOW5000 = -2500
1194 BELOW2500 = -1500
1195 BELOW500 = -1000
1196 TOLERANCE = 1.2
1197
1198 def isCondition(self, flight, aircraft, oldState, state):
1199 """Check if the fault condition holds."""
1200 vs = state.smoothedVS
1201 altitude = state.altitude
1202 return vs < -8000 or vs > 8000 or \
1203 (altitude<500 and vs < (VSChecker.BELOW500 *
1204 VSChecker.TOLERANCE)) or \
1205 (altitude<2500 and vs < (VSChecker.BELOW2500 *
1206 VSChecker.TOLERANCE)) or \
1207 (altitude<5000 and vs < (VSChecker.BELOW5000 *
1208 VSChecker.TOLERANCE)) or \
1209 (altitude<10000 and vs < (VSChecker.BELOW10000 *
1210 VSChecker.TOLERANCE))
1211
1212 def logFault(self, flight, aircraft, logger, oldState, state):
1213 """Log the fault."""
1214 vs = state.smoothedVS
1215
1216 message = "Vertical speed was %.0f feet/min" % (vs,)
1217 if vs>-8000 and vs<8000:
1218 message += " at %.0f feet (exceeds company limit)" % (state.altitude,)
1219
1220 score = 10 if vs<-8000 or vs>8000 else 0
1221
1222 flight.handleFault(VSChecker, state.timestamp,
1223 FaultChecker._appendDuring(flight, message),
1224 score)
1225
1226#---------------------------------------------------------------------------------------
Note: See TracBrowser for help on using the repository browser.