source: src/mlx/checks.py

python3
Last change on this file was 1128:3a549693a614, checked in by István Váradi <ivaradi@…>, 2 months ago

The grace period for the anti-collision lights is increased to 5 seconds (re #382)

File size: 70.4 KB
RevLine 
[298]1
[919]2from . import fs
3from . import const
4from . import util
5from .acars import ACARS
6from .sound import startSound
[298]7
8import time
9
10#---------------------------------------------------------------------------------------
11
[297]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
[8]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()
[332]39
[8]40#---------------------------------------------------------------------------------------
41
42class StageChecker(StateChecker):
43 """Check the flight stage transitions."""
[25]44 def __init__(self):
45 """Construct the stage checker."""
46 self._flareStarted = False
47
[8]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 \
[1107]55 (not state.trickMode and state.groundSpeed>5.0) or \
56 (flight.departureGateIsTaxiThrough and state.antiCollisionLightsOn):
[8]57 aircraft.setStage(state, const.STAGE_PUSHANDTAXI)
58 elif stage==const.STAGE_TAKEOFF:
59 if not state.gearsDown or \
60 (state.radioAltitude>3000.0 and state.vs>0):
61 aircraft.setStage(state, const.STAGE_CLIMB)
62 elif not state.landingLightsOn and \
[477]63 (state.strobeLightsOn is False or
[529]64 (state.strobeLightsOn is None and state.xpdrC is False)) and \
[8]65 state.onTheGround and \
66 state.groundSpeed<50.0:
67 aircraft.setStage(state, const.STAGE_RTO)
68 elif stage==const.STAGE_CLIMB:
69 if (state.altitude+2000) > flight.cruiseAltitude:
70 aircraft.setStage(state, const.STAGE_CRUISE)
71 elif state.radioAltitude<2000.0 and \
72 state.vs < 0.0 and state.gearsDown:
73 aircraft.setStage(state, const.STAGE_LANDING)
74 elif stage==const.STAGE_CRUISE:
[594]75 if (state.altitude+2000) < flight.cruiseAltitudeForDescent:
[8]76 aircraft.setStage(state, const.STAGE_DESCENT)
77 elif stage==const.STAGE_DESCENT or stage==const.STAGE_GOAROUND:
78 if state.gearsDown and state.radioAltitude<2000.0:
79 aircraft.setStage(state, const.STAGE_LANDING)
80 elif (state.altitude+2000) > flight.cruiseAltitude:
81 aircraft.setStage(state, const.STAGE_CRUISE)
82 elif stage==const.STAGE_LANDING:
[332]83 if state.onTheGround and state.groundSpeed<25.0:
[8]84 aircraft.setStage(state, const.STAGE_TAXIAFTERLAND)
85 elif not state.gearsDown:
86 aircraft.setStage(state, const.STAGE_GOAROUND)
[25]87 elif state.radioAltitude>200 and self._flareStarted:
[8]88 aircraft.cancelFlare()
[25]89 self._flareStarted = False
90 elif state.radioAltitude<150 and not state.onTheGround and \
91 not self._flareStarted:
92 self._flareStarted = True
[9]93 aircraft.prepareFlare()
[8]94 elif stage==const.STAGE_TAXIAFTERLAND:
[359]95 if state.parking and aircraft.checkFlightEnd(state):
[8]96 aircraft.setStage(state, const.STAGE_END)
97
98#---------------------------------------------------------------------------------------
99
[139]100class ACARSSender(StateChecker):
101 """Sender of online ACARS.
102
103 It sends the ACARS every 3 minutes to the MAVA website."""
104
[297]105 ## The interval at which the ACARS are sent
[139]106 INTERVAL = 3*60.0
[332]107
[139]108 def __init__(self, gui):
109 """Construct the ACARS sender."""
110 self._gui = gui
111 self._lastSent = None
[374]112 self._sending = False
[139]113
114 def check(self, flight, aircraft, logger, oldState, state):
115 """If the time has come to send the ACARS, send it."""
[374]116 if self._sending:
117 return
118
[139]119 now = time.time()
[332]120
[139]121 if self._lastSent is not None and \
122 (self._lastSent + ACARSSender.INTERVAL)>now:
[332]123 return
[139]124
[380]125 self._sending = True
[139]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:
[919]132 print("Sent online ACARS")
[139]133 self._lastSent = time.time() if self._lastSent is None \
134 else self._lastSent + ACARSSender.INTERVAL
135 else:
[919]136 print("Failed to send the ACARS")
[374]137 self._sending = False
[332]138
[139]139#---------------------------------------------------------------------------------------
140
[9]141class TakeOffLogger(StateChecker):
142 """Logger for the cruise speed."""
143 def __init__(self):
144 """Construct the logger."""
145 self._onTheGround = True
[332]146
[9]147 def check(self, flight, aircraft, logger, oldState, state):
148 """Log the cruise speed if necessary."""
149 if flight.stage==const.STAGE_TAKEOFF and \
150 self._onTheGround and not state.onTheGround:
151 logger.message(state.timestamp,
[241]152 "Takeoff speed: %.0f %s" % \
153 (flight.speedFromKnots(state.ias),
154 flight.getEnglishSpeedUnit()))
[9]155 logger.message(state.timestamp,
156 "Takeoff heading: %03.0f degrees" % (state.heading,))
157 logger.message(state.timestamp,
158 "Takeoff pitch: %.1f degrees" % (state.pitch,))
[332]159 logger.message(state.timestamp,
[364]160 "Takeoff flaps: %.0f" % (state.flapsSet))
161 logger.message(state.timestamp,
[340]162 "CG/Trim: %.1f%%/%.2f" % \
163 (state.cog*100.0, state.elevatorTrim))
[611]164
165
166 if abs(state.pitch)>aircraft.maxTakeOffPitch:
167 flight.handleNoGo("TOPitch", state.timestamp,
168 "Takeoff pitch higher than aircraft maximum (%.2f)" % \
169 (aircraft.maxTakeOffPitch,),
170 "TO TAILSTRIKE NO GO")
171
[9]172 self._onTheGround = False
173
174#---------------------------------------------------------------------------------------
175
[601]176class InitialClimbSpeedLogger(StateChecker):
177 """Logger for the initial climb speed."""
178 def __init__(self):
179 """Construct the logger."""
180 self._logged = False
181
182 def check(self, flight, aircraft, logger, oldState, state):
183 """Log the initial climb speed if the altitude is reached."""
184 if not self._logged and \
185 state.radioAltitude>=aircraft.initialClimbSpeedAltitude and \
186 flight.stage in [const.STAGE_TAKEOFF, const.STAGE_CLIMB]:
187 logger.message(state.timestamp,
[635]188 "Initial climb speed: %.0f %s - %.0f %s AGL" % \
[601]189 (flight.speedFromKnots(state.ias),
190 flight.getEnglishSpeedUnit(),
[635]191 flight.aglFromFeet(state.radioAltitude),
192 flight.getEnglishAGLUnit()))
[601]193 self._logged = True
194
195#---------------------------------------------------------------------------------------
196
[9]197class CruiseSpeedLogger(StateChecker):
198 """Logger for the cruise speed."""
199 def __init__(self):
200 """Construct the logger."""
201 self._lastTime = None
[332]202
[9]203 def check(self, flight, aircraft, logger, oldState, state):
204 """Log the cruise speed if necessary."""
205 if flight.stage==const.STAGE_CRUISE and \
206 (self._lastTime is None or \
207 (self._lastTime+800)<=state.timestamp):
208 if state.altitude>24500.0:
209 logger.message(state.timestamp,
[10]210 "Cruise speed: %.3f mach" % (state.mach,))
[9]211 else:
212 logger.message(state.timestamp,
[241]213 "Cruise speed: %.0f %s" %
214 (flight.speedFromKnots(state.ias),
215 flight.getEnglishSpeedUnit()))
[9]216 self._lastTime = state.timestamp
217
218#---------------------------------------------------------------------------------------
219
[10]220class SpoilerLogger(StateChecker):
[600]221 """Logger for the spoiler."""
[10]222 def __init__(self):
223 """Construct the logger."""
[600]224 self._spoilersDown = True
[10]225 self._logged = False
[13]226 self._spoilersExtension = None
[332]227
[10]228 def check(self, flight, aircraft, logger, oldState, state):
229 """Log the cruise speed if necessary."""
[600]230 spoilersDown = state.spoilersExtension==0
231
232 if flight.stage==const.STAGE_LANDING and state.onTheGround:
233 if not self._logged:
234 if not spoilersDown and self._spoilersDown:
235 logger.message(state.timestamp, "Speedbrake deployed")
[10]236 self._logged = True
[170]237 config = flight.config
238 if config.enableSounds and config.speedbrakeAtTD:
239 startSound(const.SOUND_SPEEDBRAKE)
[600]240 elif spoilersDown!=self._spoilersDown:
241 logger.message(state.timestamp,
242 "Speedbrake " + ("down" if spoilersDown else "up"))
243
244 self._spoilersDown = spoilersDown
[10]245
246#---------------------------------------------------------------------------------------
247
[134]248class VisibilityChecker(StateChecker):
249 """Inform the pilot of the visibility once when descending below 2000 ft,
250 then when descending below 1000 ft."""
251 def __init__(self):
252 """Construct the visibility checker."""
253 self._informedBelow2000 = False
254 self._informedBelow1000 = False
255
256 def check(self, flight, aircraft, logger, oldState, state):
257 """Check if we need to inform the pilot of the visibility."""
258 if flight.stage==const.STAGE_DESCENT or \
259 flight.stage==const.STAGE_LANDING:
260 if (state.radioAltitude<2000 and not self._informedBelow2000) or \
261 (state.radioAltitude<1000 and not self._informedBelow1000):
262 visibilityString = util.visibility2String(state.visibility)
263 fs.sendMessage(const.MESSAGETYPE_VISIBILITY,
264 "Current visibility: " + visibilityString,
265 5)
266 logger.message(state.timestamp,
267 "Pilot was informed about the visibility: " +
268 visibilityString)
269 self._informedBelow2000 = True
270 self._informedBelow1000 = state.radioAltitude<1000
271
272#---------------------------------------------------------------------------------------
273
[273]274class ApproachCalloutsPlayer(StateChecker):
275 """A state checker that plays a sequence of approach callouts.
276
277 It tracks the altitude during the descent and landing phases and
278 if the altitude crosses one that has a callout associated with and
[332]279 the vertical speed is negative, that callout will be played."""
[273]280 def __init__(self, approachCallouts):
281 """Construct the approach callouts player."""
282 self._approachCallouts = approachCallouts
283 self._altitudes = approachCallouts.getAltitudes(descending = False)
284
285 def check(self, flight, aircraft, logger, oldState, state):
286 """Check if we need to play a callout."""
287 if (flight.stage==const.STAGE_DESCENT or \
288 flight.stage==const.STAGE_LANDING) and state.vs<0:
289 oldRadioAltitude = oldState.radioAltitude
290 radioAltitude = state.radioAltitude
291 for altitude in self._altitudes:
292 if radioAltitude<=altitude and \
293 oldRadioAltitude>altitude:
294 startSound(self._approachCallouts[altitude])
295 break
296
297#---------------------------------------------------------------------------------------
298
[8]299class StateChangeLogger(StateChecker):
300 """Base class for classes the instances of which check if a specific change has
301 occured in the aircraft's state, and log such change."""
[356]302 def __init__(self, logInitial = True, excludedStages = None):
[8]303 """Construct the logger.
304
305 If logInitial is True, the initial value will be logged, not just the
306 changes later.
307
[356]308 If excludedStages is given, it should be a list containing those stages
309 during which the changes should not be logged.
310
[8]311 Child classes should define the following functions:
312 - _changed(self, oldState, state): returns a boolean indicating if the
313 value has changed or not
[321]314 - _getMessage(self, flight, state, forced): return a strings containing
315 the message to log with the new value
[8]316 """
[332]317 self._logInitial = logInitial
[356]318 self._excludedStages = [] if excludedStages is None else excludedStages
[8]319
[324]320 def _getLogTimestamp(self, state, forced):
[22]321 """Get the log timestamp."""
322 return state.timestamp
323
[8]324 def check(self, flight, aircraft, logger, oldState, state):
325 """Check if the state has changed, and if so, log the new state."""
[356]326 if flight.stage in self._excludedStages:
327 return
328
[8]329 shouldLog = False
330 if oldState is None:
331 shouldLog = self._logInitial
332 else:
333 shouldLog = self._changed(oldState, state)
[332]334
[8]335 if shouldLog:
[321]336 self.logState(flight, logger, state)
337
338 def logState(self, flight, logger, state, forced = False):
339 """Log the state."""
340 message = self._getMessage(flight, state, forced)
341 if message is not None:
[324]342 logger.message(self._getLogTimestamp(state, forced), message)
[332]343
[241]344#-------------------------------------------------------------------------------
[8]345
346class SimpleChangeMixin(object):
347 """A mixin that defines a _changed() function which simply calls a function
348 to retrieve the value two monitor for both states and compares them.
349
350 Child classes should define the following function:
351 - _getValue(state): get the value we are interested in."""
352 def _changed(self, oldState, state):
353 """Determine if the value has changed."""
[314]354 currentValue = self._getValue(state)
355 return currentValue is not None and self._getValue(oldState)!=currentValue
[8]356
357#---------------------------------------------------------------------------------------
358
359class SingleValueMixin(object):
360 """A mixin that provides a _getValue() function to query a value from the
361 state using the name of the attribute."""
362 def __init__(self, attrName):
363 """Construct the mixin with the given attribute name."""
364 self._attrName = attrName
365
366 def _getValue(self, state):
367 """Get the value of the attribute from the state."""
368 return getattr(state, self._attrName)
[332]369
[8]370#---------------------------------------------------------------------------------------
371
372class DelayedChangeMixin(object):
373 """A mixin to a StateChangeLogger that stores the old value and reports a
374 change only if the change has been there for a certain amount of time.
375
376 Child classes should define the following function:
377 - _getValue(state): get the value we are interested in."""
[22]378 def __init__(self, minDelay = 3.0, maxDelay = 10.0):
[8]379 """Construct the mixin with the given delay in seconds."""
[22]380 self._minDelay = minDelay
381 self._maxDelay = maxDelay
[8]382 self._oldValue = None
383 self._firstChange = None
[22]384 self._lastChangeState = None
[355]385 self._lastLoggedValue = None
386
387 self._logState = lambda flight, logger, state, forced = False: \
388 StateChangeLogger.logState(self, flight, logger, state,
389 forced = forced)
390 self.logState = lambda flight, logger, state, forced = False: \
391 DelayedChangeMixin.logState(self, flight, logger, state,
392 forced = forced)
[332]393
[8]394 def _changed(self, oldState, state):
395 """Determine if the value has changed."""
396 if self._oldValue is None:
397 self._oldValue = self._getValue(oldState)
[332]398
[8]399 newValue = self._getValue(state)
[321]400 if self._isDifferent(self._oldValue, newValue):
[355]401 if self._lastLoggedValue is not None and \
402 not self._isDifferent(self._lastLoggedValue, newValue):
403 self._firstChange = None
404 else:
405 if self._firstChange is None:
406 self._firstChange = state.timestamp
407 self._lastChangeState = state
[22]408 self._oldValue = newValue
409
410 if self._firstChange is not None:
[324]411 if state.timestamp >= min(self._lastChangeState.timestamp +
412 self._minDelay,
[22]413 self._firstChange + self._maxDelay):
[8]414 self._firstChange = None
415 return True
[332]416
[22]417 return False
[8]418
[324]419 def _getLogTimestamp(self, state, forced):
[22]420 """Get the log timestamp."""
[324]421 return self._lastChangeState.timestamp \
422 if not forced and self._lastChangeState is not None \
423 else state.timestamp
[8]424
[321]425 def _isDifferent(self, oldValue, newValue):
426 """Determine if the given values are different.
427
428 This default implementation checks for simple equality."""
429 return oldValue!=newValue
430
[355]431 def logState(self, flight, logger, state, forced):
432 """Log the given state.
433
434 The value belonging to that state is recorded in _lastLoggedValue.
435
436 It calls _logState to perform the real logging."""
437 self._lastLoggedValue = self._getValue(state)
438 self._logState(flight, logger, state, forced = forced)
439
[8]440#---------------------------------------------------------------------------------------
441
442class TemplateMessageMixin(object):
443 """Mixin to generate a message based on a template.
444
445 Child classes should define the following function:
446 - _getValue(state): get the value we are interested in."""
447 def __init__(self, template):
448 """Construct the mixin."""
449 self._template = template
450
[321]451 def _getMessage(self, flight, state, forced):
[8]452 """Get the message."""
[332]453 value = self._getValue(state)
[320]454 return None if value is None else self._template % (value,)
[8]455
456#---------------------------------------------------------------------------------------
457
458class GenericStateChangeLogger(StateChangeLogger, SingleValueMixin,
459 DelayedChangeMixin, TemplateMessageMixin):
460 """Base for generic state change loggers that monitor a single value in the
461 state possibly with a delay and the logged message comes from a template"""
[22]462 def __init__(self, attrName, template, logInitial = True,
[356]463 excludedStages = None, minDelay = 0.0, maxDelay = 0.0):
[8]464 """Construct the object."""
[358]465 StateChangeLogger.__init__(self, logInitial = logInitial,
466 excludedStages = excludedStages)
[8]467 SingleValueMixin.__init__(self, attrName)
[329]468 DelayedChangeMixin.__init__(self, minDelay = minDelay,
469 maxDelay = maxDelay)
[8]470 TemplateMessageMixin.__init__(self, template)
[324]471 self._getLogTimestamp = \
472 lambda state, forced: \
473 DelayedChangeMixin._getLogTimestamp(self, state, forced)
[8]474
475#---------------------------------------------------------------------------------------
476
[321]477class ForceableLoggerMixin(object):
[355]478 """A mixin for loggers that can be forced to log a certain state."""
[321]479 def forceLog(self, flight, logger, state):
480 """Force logging the given state."""
481 self.logState(flight, logger, state, forced = True)
482
483#---------------------------------------------------------------------------------------
484
[8]485class AltimeterLogger(StateChangeLogger, SingleValueMixin,
486 DelayedChangeMixin):
487 """Logger for the altimeter setting."""
488 def __init__(self):
489 """Construct the logger."""
490 StateChangeLogger.__init__(self, logInitial = True)
491 SingleValueMixin.__init__(self, "altimeter")
492 DelayedChangeMixin.__init__(self)
[324]493 self._getLogTimestamp = \
494 lambda state, forced: \
495 DelayedChangeMixin._getLogTimestamp(self, state, forced)
[8]496
[321]497 def _getMessage(self, flight, state, forced):
[8]498 """Get the message to log on a change."""
[22]499 logState = self._lastChangeState if \
500 self._lastChangeState is not None else state
[409]501 message = "Altimeter: %.2f hPa at %.0f feet" % \
[394]502 (logState.altimeter, logState.altitude)
503 if not logState.altimeterReliable:
504 message += " (u/r)"
505 return message
[8]506
507#---------------------------------------------------------------------------------------
508
[321]509class NAVLogger(StateChangeLogger, DelayedChangeMixin, ForceableLoggerMixin):
[320]510 """Logger for NAV radios.
511
[355]512 It also logs the OBS radial set."""
[356]513 excludedStages = [const.STAGE_BOARDING, const.STAGE_PUSHANDTAXI,
514 const.STAGE_RTO, const.STAGE_TAXIAFTERLAND,
515 const.STAGE_PARKING]
516
[321]517 @staticmethod
518 def getMessage(logName, frequency, obs):
519 """Get the message for the given NAV radio setting."""
[919]520 message = "%-5s %s" % (logName + ":", frequency)
521 if obs is not None: message += " (%03d\u00b0)" % (obs,)
[321]522 return message
[332]523
[320]524 def __init__(self, attrName, logName):
525 """Construct the NAV logger."""
[356]526 StateChangeLogger.__init__(self, logInitial = False,
527 excludedStages = self.excludedStages)
[320]528 DelayedChangeMixin.__init__(self)
[321]529
530 self._getLogTimestamp = \
[324]531 lambda state, forced: \
532 DelayedChangeMixin._getLogTimestamp(self, state, forced)
[320]533
534 self._attrName = attrName
535 self._logName = logName
[321]536
[320]537 def _getValue(self, state):
538 """Get the value.
539
540 If both the frequency and the obs settings are available, a tuple
541 containing them is returned, otherwise None."""
542 frequency = getattr(state, self._attrName)
543 obs = getattr(state, self._attrName + "_obs")
[321]544 manual = getattr(state, self._attrName + "_manual")
545 return (frequency, obs, manual)
[320]546
[321]547 def _getMessage(self, flight, state, forced):
[320]548 """Get the message."""
[321]549 (frequency, obs, manual) = self._getValue(state)
550 return None if frequency is None or obs is None or \
551 (not manual and not forced) else \
552 self.getMessage(self._logName, frequency, obs)
553
554 def _isDifferent(self, oldValue, newValue):
555 """Determine if the valie has changed between the given states."""
556 (oldFrequency, oldOBS, _oldManual) = oldValue
557 (newFrequency, newOBS, _newManual) = newValue
558 return oldFrequency!=newFrequency or oldOBS!=newOBS
[320]559
560#---------------------------------------------------------------------------------------
561
[366]562class ILSLogger(NAVLogger):
563 """Logger for the ILS radio setting."""
564 def __init__(self):
565 """Construct the logger."""
566 super(ILSLogger, self).__init__("ils", "ILS")
567
568#---------------------------------------------------------------------------------------
569
[320]570class NAV1Logger(NAVLogger):
[8]571 """Logger for the NAV1 radio setting."""
572 def __init__(self):
573 """Construct the logger."""
[320]574 super(NAV1Logger, self).__init__("nav1", "NAV1")
[8]575
576#---------------------------------------------------------------------------------------
577
[320]578class NAV2Logger(NAVLogger):
[8]579 """Logger for the NAV2 radio setting."""
580 def __init__(self):
581 """Construct the logger."""
[320]582 super(NAV2Logger, self).__init__("nav2", "NAV2")
[8]583
584#---------------------------------------------------------------------------------------
585
[321]586class ADFLogger(GenericStateChangeLogger, ForceableLoggerMixin):
587 """Base class for the ADF loggers."""
588 def __init__(self, attr, logName):
589 """Construct the ADF logger."""
590 GenericStateChangeLogger.__init__(self, attr,
[333]591 "%s: %%s" % (logName,),
[356]592 logInitial = False,
593 excludedStages =
594 NAVLogger.excludedStages,
[321]595 minDelay = 3.0, maxDelay = 10.0)
596
597#---------------------------------------------------------------------------------------
598
599class ADF1Logger(ADFLogger):
[317]600 """Logger for the ADF1 radio setting."""
601 def __init__(self):
602 """Construct the logger."""
[321]603 super(ADF1Logger, self).__init__("adf1", "ADF1")
[317]604
605#---------------------------------------------------------------------------------------
606
[321]607class ADF2Logger(ADFLogger):
[317]608 """Logger for the ADF2 radio setting."""
609 def __init__(self):
610 """Construct the logger."""
[321]611 super(ADF2Logger, self).__init__("adf2", "ADF2")
[317]612
613#---------------------------------------------------------------------------------------
614
[8]615class SquawkLogger(GenericStateChangeLogger):
616 """Logger for the squawk setting."""
617 def __init__(self):
618 """Construct the logger."""
619 super(SquawkLogger, self).__init__("squawk", "Squawk code: %s",
[22]620 minDelay = 3.0, maxDelay = 10.0)
[8]621
622#---------------------------------------------------------------------------------------
623
[562]624class LightsLogger(StateChangeLogger, SingleValueMixin, DelayedChangeMixin):
[8]625 """Base class for the loggers of the various lights."""
626 def __init__(self, attrName, template):
627 """Construct the logger."""
628 StateChangeLogger.__init__(self)
629 SingleValueMixin.__init__(self, attrName)
[562]630 DelayedChangeMixin.__init__(self)
631 self._getLogTimestamp = \
632 lambda state, forced: \
633 DelayedChangeMixin._getLogTimestamp(self, state, forced)
[332]634
[8]635 self._template = template
636
[321]637 def _getMessage(self, flight, state, forced):
[8]638 """Get the message from the given state."""
[416]639 value = self._getValue(state)
640 if value is None:
641 return None
642 else:
643 return self._template % ("ON" if value else "OFF")
[332]644
[8]645#---------------------------------------------------------------------------------------
646
647class AnticollisionLightsLogger(LightsLogger):
648 """Logger for the anti-collision lights."""
649 def __init__(self):
650 LightsLogger.__init__(self, "antiCollisionLightsOn",
651 "Anti-collision lights: %s")
652
653#---------------------------------------------------------------------------------------
654
655class LandingLightsLogger(LightsLogger):
656 """Logger for the landing lights."""
657 def __init__(self):
658 LightsLogger.__init__(self, "landingLightsOn",
659 "Landing lights: %s")
660
661#---------------------------------------------------------------------------------------
662
663class StrobeLightsLogger(LightsLogger):
664 """Logger for the strobe lights."""
665 def __init__(self):
666 LightsLogger.__init__(self, "strobeLightsOn",
667 "Strobe lights: %s")
668
669#---------------------------------------------------------------------------------------
670
671class NavLightsLogger(LightsLogger):
672 """Logger for the navigational lights."""
673 def __init__(self):
674 LightsLogger.__init__(self, "navLightsOn",
675 "Navigational lights: %s")
676
677#---------------------------------------------------------------------------------------
678
[520]679class FlapsLogger(StateChangeLogger, SingleValueMixin, DelayedChangeMixin):
[8]680 """Logger for the flaps setting."""
681 def __init__(self):
682 """Construct the logger."""
[364]683 StateChangeLogger.__init__(self, logInitial = True,
684 excludedStages =
685 [const.STAGE_BOARDING,
686 const.STAGE_PUSHANDTAXI,
687 const.STAGE_RTO,
688 const.STAGE_TAKEOFF])
[8]689 SingleValueMixin.__init__(self, "flapsSet")
[520]690 DelayedChangeMixin.__init__(self)
691 self._getLogTimestamp = \
692 lambda state, forced: \
693 DelayedChangeMixin._getLogTimestamp(self, state, forced)
[8]694
[321]695 def _getMessage(self, flight, state, forced):
[8]696 """Get the message to log on a change."""
[520]697 logState = self._lastChangeState if \
698 self._lastChangeState is not None else state
699 speed = logState.groundSpeed if logState.groundSpeed<80.0 \
700 else logState.ias
[635]701 message = "Flaps %.0f - %.0f %s, %.0f %s AGL, %.0f ft AMSL" % \
[520]702 (logState.flapsSet, flight.speedFromKnots(speed),
[635]703 flight.getEnglishSpeedUnit(),
704 flight.aglFromFeet(logState.radioAltitude),
705 flight.getEnglishAGLUnit(),
[602]706 logState.altitude)
[525]707
708 return message
709
[8]710#---------------------------------------------------------------------------------------
711
712class GearsLogger(StateChangeLogger, SingleValueMixin, SimpleChangeMixin):
713 """Logger for the gears state."""
714 def __init__(self):
715 """Construct the logger."""
716 StateChangeLogger.__init__(self, logInitial = True)
[209]717 SingleValueMixin.__init__(self, "gearControlDown")
[8]718
[321]719 def _getMessage(self, flight, state, forced):
[8]720 """Get the message to log on a change."""
[635]721 return "Gears SET to %s at %.0f %s, %.0f ft, %.0f %s AGL" % \
[209]722 ("DOWN" if state.gearControlDown else "UP",
[241]723 flight.speedFromKnots(state.ias),
[525]724 flight.getEnglishSpeedUnit(),
[635]725 state.altitude,
726 flight.aglFromFeet(state.radioAltitude),
727 flight.getEnglishAGLUnit())
[8]728
729#---------------------------------------------------------------------------------------
[11]730
[338]731class APLogger(StateChangeLogger, DelayedChangeMixin):
732 """Log the state of the autopilot."""
733 @staticmethod
734 def _logBoolean(logger, timestamp, what, value):
735 """Log a boolean value.
736
737 what is the name of the value that is being logged."""
738 message = what + " "
739 if value is None:
740 message += "cannot be detected, will not log"
741 else:
742 message += "is " + ("ON" if value else "OFF")
743 logger.message(timestamp, message)
744
745 @staticmethod
746 def _logNumeric(logger, timestamp, what, format, value):
747 """Log a numerical value."""
748 message = what
749 if value is None:
[919]750 message += " cannot be detected, will not log"
[338]751 else:
[919]752 message += ": " + format % (value,)
[338]753 logger.message(timestamp, message)
754
755 @staticmethod
756 def _logAPMaster(logger, timestamp, state):
757 """Log the AP master state."""
758 APLogger._logBoolean(logger, timestamp, "AP master",
759 state.apMaster)
760
761 @staticmethod
762 def _logAPHeadingHold(logger, timestamp, state):
763 """Log the AP heading hold state."""
764 APLogger._logBoolean(logger, timestamp, "AP heading hold",
765 state.apHeadingHold)
766
767 @staticmethod
768 def _logAPHeading(logger, timestamp, state):
769 """Log the AP heading."""
[919]770 APLogger._logNumeric(logger, timestamp, "AP heading",
771 "%03.0f\u00b0", state.apHeading)
[338]772
773 @staticmethod
774 def _logAPAltitudeHold(logger, timestamp, state):
775 """Log the AP altitude hold state."""
776 APLogger._logBoolean(logger, timestamp, "AP altitude hold",
777 state.apAltitudeHold)
778
779 @staticmethod
780 def _logAPAltitude(logger, timestamp, state):
781 """Log the AP heading."""
[919]782 APLogger._logNumeric(logger, timestamp, "AP altitude",
783 "%.0f ft", state.apAltitude)
[338]784
785 def __init__(self):
786 """Construct the state logger."""
787 StateChangeLogger.__init__(self)
788 DelayedChangeMixin.__init__(self)
789 self._lastLoggedState = None
[355]790 self.logState = lambda flight, logger, state:\
791 APLogger.logState(self, flight, logger, state)
[338]792
793 def _getValue(self, state):
794 """Convert the relevant values from the given state into a tuple."""
795 return (state.apMaster,
796 state.apHeadingHold, state.apHeading,
797 state.apAltitudeHold, state.apAltitude)
798
799 def _isDifferent(self, oldValue, newValue):
800 """Determine if the given old and new values are different (enough) to
801 be logged."""
802 (oldAPMaster, oldAPHeadingHold, oldAPHeading,
803 oldAPAltitudeHold, oldAPAltitude) = oldValue
804 (apMaster, apHeadingHold, apHeading,
805 apAltitudeHold, apAltitude) = newValue
806
807 if apMaster is not None and apMaster!=oldAPMaster:
808 return True
809 if apMaster is False:
810 return False
811
812 if apHeadingHold is not None and apHeadingHold!=oldAPHeadingHold:
813 return True
814 if apHeadingHold is not False and apHeading is not None and \
815 apHeading!=oldAPHeading:
816 return True
817
818 if apAltitudeHold is not None and apAltitudeHold!=oldAPAltitudeHold:
819 return True
820 if apAltitudeHold is not False and apAltitude is not None and \
821 apAltitude!=oldAPAltitude:
822 return True
823
824 return False
825
826 def logState(self, flight, logger, state):
827 """Log the autopilot state."""
828 timestamp = DelayedChangeMixin._getLogTimestamp(self, state, False)
829 if self._lastLoggedState is None:
830 self._logAPMaster(logger, timestamp, state)
831 if state.apMaster is not False or state.apHeadingHold is None:
832 self._logAPHeadingHold(logger, timestamp, state)
833 if state.apMaster is not False and \
834 (state.apHeadingHold is not False or state.apHeading is None):
835 self._logAPHeading(logger, timestamp, state)
836 if state.apMaster is not False or state.apAltitudeHold is None:
837 self._logAPAltitudeHold(logger, timestamp, state)
838 if state.apMaster is not False and \
839 (state.apAltitudeHold is not False or state.apAltitude is None):
840 self._logAPAltitude(logger, timestamp, state)
841 self._firstCall = False
842 else:
843 oldState = self._lastLoggedState
844
845 apMasterTurnedOn = False
846 if state.apMaster is not None and state.apMaster!=oldState.apMaster:
847 apMasterTurnedOn = state.apMaster
848 self._logAPMaster(logger, timestamp, state)
849
850 if state.apMaster is not False:
851 apHeadingHoldTurnedOn = False
852 if state.apHeadingHold is not None and \
853 (state.apHeadingHold!=oldState.apHeadingHold or
854 apMasterTurnedOn):
855 apHeadingHoldTurnedOn = state.apHeadingHold
856 self._logAPHeadingHold(logger, timestamp, state)
857
858 if state.apHeadingHold is not False and \
859 state.apHeading is not None and \
860 (state.apHeading!=oldState.apHeading or apMasterTurnedOn or
861 apHeadingHoldTurnedOn):
862 self._logAPHeading(logger, timestamp, state)
863
864 apAltitudeHoldTurnedOn = False
865 if state.apAltitudeHold is not None and \
866 (state.apAltitudeHold!=oldState.apAltitudeHold or
867 apMasterTurnedOn):
868 apAltitudeHoldTurnedOn = state.apAltitudeHold
869 self._logAPAltitudeHold(logger, timestamp, state)
870
871 if state.apAltitudeHold is not False and \
872 state.apAltitude is not None and \
873 (state.apAltitude!=oldState.apAltitude or apMasterTurnedOn or
874 apAltitudeHoldTurnedOn):
875 self._logAPAltitude(logger, timestamp, state)
876
877 self._lastLoggedState = state
878
879#---------------------------------------------------------------------------------------
880
[11]881class FaultChecker(StateChecker):
882 """Base class for checkers that look for faults."""
883 @staticmethod
884 def _appendDuring(flight, message):
885 """Append a 'during XXX' test to the given message, depending on the
886 flight stage."""
887 stageStr = const.stage2string(flight.stage)
888 return message if stageStr is None \
889 else (message + " during " + stageStr.upper())
890
891 @staticmethod
892 def _getLinearScore(minFaultValue, maxFaultValue, minScore, maxScore,
893 value):
894 """Get the score for a faulty value where the score is calculated
895 linearly within a certain range."""
896 if value<minFaultValue:
897 return 0
898 elif value>maxFaultValue:
899 return maxScore
900 else:
901 return minScore + (maxScore-minScore) * (value-minFaultValue) / \
902 (maxFaultValue - minFaultValue)
903
904#---------------------------------------------------------------------------------------
905
906class SimpleFaultChecker(FaultChecker):
907 """Base class for fault checkers that check for a single occurence of a
908 faulty condition.
909
910 Child classes should implement the following functions:
911 - isCondition(self, flight, aircraft, oldState, state): should return whether the
912 condition holds
913 - logFault(self, flight, aircraft, logger, oldState, state): log the fault
914 via the logger."""
915 def check(self, flight, aircraft, logger, oldState, state):
916 """Perform the check."""
917 if self.isCondition(flight, aircraft, oldState, state):
918 self.logFault(flight, aircraft, logger, oldState, state)
919
920#---------------------------------------------------------------------------------------
921
922class PatientFaultChecker(FaultChecker):
923 """A fault checker that does not decides on a fault when the condition
924 arises immediately, but can wait some time.
925
926 Child classes should implement the following functions:
927 - isCondition(self, flight, aircraft, oldState, state): should return whether the
928 condition holds
929 - logFault(self, flight, aircraft, logger, oldState, state): log the fault
930 via the logger
931 """
932 def __init__(self, timeout = 2.0):
933 """Construct the fault checker with the given timeout."""
934 self._timeout = timeout
935 self._faultStarted = None
936
937 def getTimeout(self, flight, aircraft, oldState, state):
938 """Get the timeout.
939
940 This default implementation returns the timeout given in the
941 constructor, but child classes might want to enforce a different
942 policy."""
943 return self._timeout
944
945 def check(self, flight, aircraft, logger, oldState, state):
946 """Perform the check."""
947 if self.isCondition(flight, aircraft, oldState, state):
948 if self._faultStarted is None:
949 self._faultStarted = state.timestamp
950 timeout = self.getTimeout(flight, aircraft, oldState, state)
951 if state.timestamp>=(self._faultStarted + timeout):
952 self.logFault(flight, aircraft, logger, oldState, state)
953 self._faultStarted = state.timestamp
954 else:
955 self._faultStarted = None
[332]956
[11]957#---------------------------------------------------------------------------------------
958
959class AntiCollisionLightsChecker(PatientFaultChecker):
960 """Check for the anti-collision light being off at high N1 values."""
[1128]961 def __init__(self):
962 """Construct the anti-collision lights checker."""
963 super(AntiCollisionLightsChecker, self).__init__(timeout = 5.0)
964
[11]965 def isCondition(self, flight, aircraft, oldState, state):
966 """Check if the fault condition holds."""
[519]967 return (not flight.config.usingFS2Crew or not state.parking or
968 flight.stage!=const.STAGE_TAXIAFTERLAND) and \
[661]969 state.antiCollisionLightsOn is False and \
[211]970 self.isEngineCondition(state)
[11]971
972 def logFault(self, flight, aircraft, logger, oldState, state):
973 """Log the fault."""
[30]974 flight.handleFault(AntiCollisionLightsChecker, state.timestamp,
975 FaultChecker._appendDuring(flight,
[548]976 "Anti-collision lights were %s" %
977 ("on" if state.antiCollisionLightsOn else "off",)),
[30]978 1)
[11]979
[211]980 def isEngineCondition(self, state):
981 """Determine if the engines are in such a state that the lights should
982 be on."""
[263]983 if state.n1 is not None:
[1043]984 for n1 in state.n1:
985 if n1 is not None and n1>5:
986 return True
987 return False
[263]988 elif state.rpm is not None:
989 return max(state.rpm)>0
990 else:
991 return False
[211]992
993#---------------------------------------------------------------------------------------
994
995class TupolevAntiCollisionLightsChecker(AntiCollisionLightsChecker):
996 """Check for the anti-collision light for Tuplev planes."""
[307]997 def isCondition(self, flight, aircraft, oldState, state):
[332]998 """Check if the fault condition holds."""
[307]999 numEnginesRunning = 0
1000 for n1 in state.n1:
1001 if n1>5: numEnginesRunning += 1
[332]1002
[452]1003 if flight.stage==const.STAGE_PARKING or \
[548]1004 flight.stage==const.STAGE_TAXIAFTERLAND:
[307]1005 return numEnginesRunning<len(state.n1) \
1006 and state.antiCollisionLightsOn
1007 else:
1008 return numEnginesRunning>1 and not state.antiCollisionLightsOn or \
1009 numEnginesRunning<1 and state.antiCollisionLightsOn
[211]1010
[11]1011#---------------------------------------------------------------------------------------
1012
[631]1013class TupolevLandingLightsChecker(PatientFaultChecker):
1014 """Check if the landing light is not switched on above an IAS of 340 km/h."""
1015 def isCondition(self, flight, aircraft, oldState, state):
1016 """Check if the fault condition holds."""
1017 return state.landingLightsOn and state.ias>(340.0*const.KMPHTOKNOTS)
1018
1019 def logFault(self, flight, aircraft, logger, oldState, state):
1020 """Log the fault."""
1021 flight.handleFault(TupolevLandingLightsChecker, state.timestamp,
1022 "The landing lights were on above an IAS of 340 km/h",
1023 1)
1024
1025#---------------------------------------------------------------------------------------
1026
[11]1027class BankChecker(SimpleFaultChecker):
[263]1028 """Check for the bank is within limits."""
[11]1029 def isCondition(self, flight, aircraft, oldState, state):
1030 """Check if the fault condition holds."""
[912]1031 isXPlane = (flight.aircraftType==const.AIRCRAFT_DH8D or
1032 flight.aircraftType==const.AIRCRAFT_B733) and \
[1073]1033 (flight.fsType==const.SIM_XPLANE12 or
1034 flight.fsType==const.SIM_XPLANE11 or
[912]1035 flight.fsType==const.SIM_XPLANE10 or
1036 flight.fsType==const.SIM_XPLANE9)
[11]1037 if flight.stage==const.STAGE_CRUISE:
[912]1038 bankLimit = 40 if isXPlane else 30
[11]1039 elif flight.stage in [const.STAGE_TAKEOFF, const.STAGE_CLIMB,
1040 const.STAGE_DESCENT, const.STAGE_LANDING]:
[912]1041 bankLimit = 45 if isXPlane else 35
[11]1042 else:
1043 return False
1044
1045 return state.bank>bankLimit or state.bank<-bankLimit
1046
1047 def logFault(self, flight, aircraft, logger, oldState, state):
1048 """Log the fault."""
[420]1049 message = "Bank too steep (%.1f)" % (state.bank,)
[30]1050 flight.handleFault(BankChecker, state.timestamp,
[420]1051 FaultChecker._appendDuring(flight, message),
[30]1052 2)
[11]1053
1054#---------------------------------------------------------------------------------------
1055
1056class FlapsRetractChecker(SimpleFaultChecker):
1057 """Check if the flaps are not retracted too early."""
[13]1058 def __init__(self):
1059 """Construct the flaps checker."""
1060 self._timeStart = None
[332]1061
[11]1062 def isCondition(self, flight, aircraft, oldState, state):
1063 """Check if the fault condition holds.
1064
1065 FIXME: check if this really is the intention (FlapsRetractedMistake.java)"""
[357]1066 if (flight.stage==const.STAGE_TAKEOFF and not state.onTheGround and
1067 aircraft.type!=const.AIRCRAFT_F70) or \
[11]1068 (flight.stage==const.STAGE_LANDING and state.onTheGround):
1069 if self._timeStart is None:
1070 self._timeStart = state.timestamp
1071
1072 if state.flapsSet==0 and state.timestamp<=(self._timeStart+2.0):
1073 return True
[13]1074 else:
1075 self._timeStart = None
[11]1076 return False
1077
1078 def logFault(self, flight, aircraft, logger, oldState, state):
1079 """Log the fault."""
[30]1080 flight.handleFault(FlapsRetractChecker, state.timestamp,
1081 FaultChecker._appendDuring(flight, "Flaps retracted"),
1082 20)
[11]1083
1084#---------------------------------------------------------------------------------------
1085
1086class FlapsSpeedLimitChecker(SimpleFaultChecker):
1087 """Check if the flaps are extended only at the right speeds."""
1088 def isCondition(self, flight, aircraft, oldState, state):
1089 """Check if the fault condition holds."""
1090 speedLimit = aircraft.getFlapsSpeedLimit(state.flapsSet)
[197]1091 return speedLimit is not None and state.smoothedIAS>speedLimit
[11]1092
1093 def logFault(self, flight, aircraft, logger, oldState, state):
1094 """Log the fault."""
[30]1095 flight.handleFault(FlapsSpeedLimitChecker, state.timestamp,
1096 FaultChecker._appendDuring(flight, "Flap speed limit fault"),
1097 5)
[11]1098
1099#---------------------------------------------------------------------------------------
1100
1101class GearsDownChecker(SimpleFaultChecker):
1102 """Check if the gears are down at low altitudes."""
1103 def isCondition(self, flight, aircraft, oldState, state):
1104 """Check if the fault condition holds."""
1105 return state.radioAltitude<10 and not state.gearsDown and \
1106 flight.stage!=const.STAGE_TAKEOFF
1107
1108 def logFault(self, flight, aircraft, logger, oldState, state):
1109 """Log the fault."""
[30]1110 flight.handleNoGo(GearsDownChecker, state.timestamp,
1111 "Gears not down at %.0f feet radio altitude" % \
1112 (state.radioAltitude,),
1113 "GEAR DOWN NO GO")
[11]1114
1115#---------------------------------------------------------------------------------------
1116
1117class GearSpeedLimitChecker(PatientFaultChecker):
1118 """Check if the gears not down at too high a speed."""
1119 def isCondition(self, flight, aircraft, oldState, state):
1120 """Check if the fault condition holds."""
[197]1121 return state.gearsDown and state.smoothedIAS>aircraft.gearSpeedLimit
[11]1122
1123 def logFault(self, flight, aircraft, logger, oldState, state):
1124 """Log the fault."""
[30]1125 flight.handleFault(GearSpeedLimitChecker, state.timestamp,
1126 FaultChecker._appendDuring(flight, "Gear speed limit fault"),
1127 5)
[11]1128
1129#---------------------------------------------------------------------------------------
1130
1131class GLoadChecker(SimpleFaultChecker):
1132 """Check if the G-load does not exceed 2 except during flare."""
1133 def isCondition(self, flight, aircraft, oldState, state):
1134 """Check if the fault condition holds."""
[448]1135 return state.gLoad>2.0 and not state.onTheGround and \
1136 (flight.stage!=const.STAGE_LANDING or state.radioAltitude>=50)
[332]1137
[11]1138 def logFault(self, flight, aircraft, logger, oldState, state):
1139 """Log the fault."""
[30]1140 flight.handleFault(GLoadChecker, state.timestamp,
1141 "G-load was %.2f" % (state.gLoad,),
1142 10)
[11]1143
1144#---------------------------------------------------------------------------------------
1145
1146class LandingLightsChecker(PatientFaultChecker):
1147 """Check if the landing lights are used properly."""
1148 def getTimeout(self, flight, aircraft, oldState, state):
1149 """Get the timeout.
1150
1151 It is the default timeout except for landing and takeoff."""
1152 return 0.0 if flight.stage in [const.STAGE_TAKEOFF,
1153 const.STAGE_LANDING] else self._timeout
1154
1155 def isCondition(self, flight, aircraft, oldState, state):
1156 """Check if the fault condition holds."""
[314]1157 return state.landingLightsOn is not None and \
1158 ((flight.stage==const.STAGE_BOARDING and \
1159 state.landingLightsOn and state.onTheGround) or \
1160 (flight.stage==const.STAGE_TAKEOFF and \
1161 not state.landingLightsOn and not state.onTheGround) or \
1162 (flight.stage in [const.STAGE_CLIMB, const.STAGE_CRUISE,
1163 const.STAGE_DESCENT] and \
1164 state.landingLightsOn and state.altitude>12500) or \
1165 (flight.stage==const.STAGE_LANDING and \
[415]1166 not state.landingLightsOn and state.onTheGround and
[637]1167 state.ias>50.0) or \
[314]1168 (flight.stage==const.STAGE_PARKING and \
1169 state.landingLightsOn and state.onTheGround))
[332]1170
[11]1171 def logFault(self, flight, aircraft, logger, oldState, state):
1172 """Log the fault."""
[375]1173 score = 0 if flight.stage in [const.STAGE_TAKEOFF,
1174 const.STAGE_LANDING] else 1
[335]1175 message = "Landing lights were %s" % \
1176 (("on" if state.landingLightsOn else "off"),)
[30]1177 flight.handleFault(LandingLightsChecker, state.timestamp,
1178 FaultChecker._appendDuring(flight, message),
[332]1179 score)
[11]1180
1181#---------------------------------------------------------------------------------------
1182
[335]1183class TransponderChecker(PatientFaultChecker):
1184 """Check if the transponder is used properly."""
[361]1185 def __init__(self):
1186 """Construct the transponder checker."""
1187 super(TransponderChecker, self).__init__()
1188 self._liftOffTime = None
1189
[335]1190 def isCondition(self, flight, aircraft, oldState, state):
1191 """Check if the fault condition holds."""
[361]1192 if state.onTheGround:
1193 self._liftOffTime = None
1194 elif self._liftOffTime is None:
1195 self._liftOffTime = state.timestamp
1196
[335]1197 return state.xpdrC is not None and \
1198 ((state.xpdrC and flight.stage in
1199 [const.STAGE_BOARDING, const.STAGE_PARKING]) or \
[361]1200 (not state.xpdrC and
1201 (flight.stage in
1202 [const.STAGE_CRUISE, const.STAGE_DESCENT,
[446]1203 const.STAGE_GOAROUND] or \
[518]1204 (flight.stage==const.STAGE_LANDING and
[446]1205 state.groundSpeed>50.0) or \
[361]1206 ((not state.autoXPDR or \
1207 (self._liftOffTime is not None and
1208 state.timestamp > (self._liftOffTime+8))) and \
[518]1209 ((flight.stage==const.STAGE_TAKEOFF and
1210 not state.onTheGround) or flight.stage==const.STAGE_CLIMB))
[361]1211 )
1212 )
1213 )
[335]1214
1215 def logFault(self, flight, aircraft, logger, oldState, state):
1216 """Log the fault."""
1217 score = 0
1218 message = "Transponder was %s" % \
1219 (("mode C" if state.xpdrC else "standby"),)
1220 flight.handleFault(TransponderChecker, state.timestamp,
1221 FaultChecker._appendDuring(flight, message),
1222 score)
1223
1224#---------------------------------------------------------------------------------------
1225
[11]1226class WeightChecker(PatientFaultChecker):
1227 """Base class for checkers that check that some limit is not exceeded."""
1228 def __init__(self, name):
1229 """Construct the checker."""
1230 super(WeightChecker, self).__init__(timeout = 5.0)
1231 self._name = name
1232
1233 def isCondition(self, flight, aircraft, oldState, state):
1234 """Check if the fault condition holds."""
1235 if flight.entranceExam:
1236 return False
1237
1238 limit = self.getLimit(flight, aircraft, state)
1239 if limit is not None:
[183]1240 #if flight.options.compensation is not None:
1241 # limit += flight.options.compensation
[13]1242 return self.getWeight(state)>limit
[11]1243
1244 return False
1245
1246 def logFault(self, flight, aircraft, logger, oldState, state):
1247 """Log the fault."""
[13]1248 mname = "M" + self._name
[30]1249 flight.handleNoGo(self.__class__, state.timestamp,
1250 "%s exceeded: %s is %.0f kg" % \
1251 (mname, self._name, self.getWeight(state)),
1252 "%s NO GO" % (mname,))
[13]1253
1254 def getWeight(self, state):
1255 """Get the weight that is interesting for us."""
1256 return state.grossWeight
[11]1257
1258#---------------------------------------------------------------------------------------
1259
1260class MLWChecker(WeightChecker):
1261 """Checks if the MLW is not exceeded on landing."""
1262 def __init__(self):
1263 """Construct the checker."""
[13]1264 super(MLWChecker, self).__init__("LW")
[11]1265
[13]1266 def getLimit(self, flight, aircraft, state):
[11]1267 """Get the limit if we are in the right state."""
1268 return aircraft.mlw if flight.stage==const.STAGE_LANDING and \
[184]1269 state.onTheGround and \
1270 not flight.entranceExam else None
[11]1271
1272#---------------------------------------------------------------------------------------
1273
1274class MTOWChecker(WeightChecker):
1275 """Checks if the MTOW is not exceeded on landing."""
1276 def __init__(self):
1277 """Construct the checker."""
[13]1278 super(MTOWChecker, self).__init__("TOW")
[11]1279
[13]1280 def getLimit(self, flight, aircraft, state):
[11]1281 """Get the limit if we are in the right state."""
[184]1282 return aircraft.mtow if flight.stage==const.STAGE_TAKEOFF and \
1283 not flight.entranceExam else None
[11]1284
1285#---------------------------------------------------------------------------------------
1286
1287class MZFWChecker(WeightChecker):
[60]1288 """Checks if the MZFW is not exceeded on landing."""
[11]1289 def __init__(self):
1290 """Construct the checker."""
[13]1291 super(MZFWChecker, self).__init__("ZFW")
[11]1292
[13]1293 def getLimit(self, flight, aircraft, state):
[11]1294 """Get the limit if we are in the right state."""
[184]1295 return aircraft.mzfw if not flight.entranceExam else None
[11]1296
[13]1297 def getWeight(self, state):
1298 """Get the weight that is interesting for us."""
1299 return state.zfw
1300
[11]1301#---------------------------------------------------------------------------------------
1302
1303class NavLightsChecker(PatientFaultChecker):
1304 """Check if the navigational lights are used properly."""
[1059]1305 def __init__(self):
1306 """Construct the NAV lights checker."""
1307 super(NavLightsChecker, self).__init__(timeout = 5.0)
1308
[11]1309 def isCondition(self, flight, aircraft, oldState, state):
1310 """Check if the fault condition holds."""
1311 return flight.stage!=const.STAGE_BOARDING and \
1312 flight.stage!=const.STAGE_PARKING and \
[341]1313 state.navLightsOn is False
[332]1314
[11]1315 def logFault(self, flight, aircraft, logger, oldState, state):
1316 """Log the fault."""
[30]1317 flight.handleFault(NavLightsChecker, state.timestamp,
1318 FaultChecker._appendDuring(flight,
1319 "Navigation lights were off"),
1320 1)
[11]1321
1322#---------------------------------------------------------------------------------------
1323
[622]1324class WindSensitiveFaultChecker(FaultChecker):
1325 """A fault checker which checks for a fault condition that might arise due
1326 to suddenly changing winds.
1327
1328 If the condition is detected, its fact is logged, and the winds are also
1329 logged repeatedly until and for a while after the condition has ceased. A
1330 fault is logged only if the condition holds for a certain period of
1331 time."""
1332 # The time the winds were logged last. This is global to avoid duplicate
1333 # logging if several wind-sensitive fault conditions gold
1334 _windsLastLogged = None
1335
1336 # The wind logging interval
1337 _windsLogInterval = 5.0
1338
1339 def __init__(self, faultTimeout = 30.0, afterLoggingTime = 20.0):
1340 """Construct the fault checker."""
1341 self._faultTimeout = faultTimeout
1342 self._afterLoggingTime = afterLoggingTime
1343
1344 self._faultStarted = None
1345 self._faultCeased = None
1346
1347 def check(self, flight, aircraft, logger, oldState, state):
1348 """Check for the condition and do whatever is needed."""
1349 timestamp = state.timestamp
1350 logWinds = False
1351
1352 if self.isCondition(flight, aircraft, oldState, state):
1353 logWinds = True
1354 if self._faultStarted is None:
1355 self._faultStarted = timestamp
1356 self._faultCeased = None
1357 self.logCondition(flight, aircraft, logger, oldState, state,
1358 False)
1359
1360 WindSensitiveFaultChecker._windsLastLogged = None
1361
1362 elif timestamp >= (self._faultStarted + self._faultTimeout):
1363 self.logCondition(flight, aircraft, logger, oldState, state,
1364 True)
1365 self._faultStarted = timestamp
1366
1367 else:
1368 if self._faultStarted is not None and self._faultCeased is None:
1369 self._faultCeased = timestamp
1370 self._faultStarted = None
1371
1372 if self._faultCeased is not None:
1373 if timestamp < (self._faultCeased + self._afterLoggingTime):
1374 logWinds = True
1375 else:
1376 self._faultCeased = None
1377
1378 if logWinds and \
1379 (WindSensitiveFaultChecker._windsLastLogged is None or
1380 timestamp >= (WindSensitiveFaultChecker._windsLastLogged +
1381 self._windsLogInterval)):
1382 logger.message(timestamp, "Winds: %.f knots from %.f" %
1383 (state.windSpeed, state.windDirection))
1384 WindSensitiveFaultChecker._windsLastLogged = timestamp
1385
1386#---------------------------------------------------------------------------------------
1387
1388class OverspeedChecker(WindSensitiveFaultChecker):
[11]1389 """Check if Vne has been exceeded."""
1390 def isCondition(self, flight, aircraft, oldState, state):
1391 """Check if the fault condition holds."""
1392 return state.overspeed
[332]1393
[622]1394 def logCondition(self, flight, aircraft, logger, oldState, state, isFault):
1395 """Log the condition fault."""
1396 if isFault:
1397 flight.handleFault(OverspeedChecker, state.timestamp,
1398 FaultChecker._appendDuring(flight, "Overspeed"),
1399 20)
1400 else:
1401 logger.message(state.timestamp,
1402 FaultChecker._appendDuring(flight, "Overspeed"))
1403
1404#---------------------------------------------------------------------------------------
1405
1406class StallChecker(WindSensitiveFaultChecker):
1407 """Check if stall occured."""
1408 def isCondition(self, flight, aircraft, oldState, state):
1409 """Check if the fault condition holds."""
1410 return flight.stage in [const.STAGE_TAKEOFF, const.STAGE_CLIMB,
1411 const.STAGE_CRUISE, const.STAGE_DESCENT,
1412 const.STAGE_LANDING] and state.stalled
1413
1414 def logCondition(self, flight, aircraft, logger, oldState, state, isFault):
1415 """Log the condition."""
1416 if isFault:
1417 score = 40 if flight.stage in [const.STAGE_TAKEOFF,
1418 const.STAGE_LANDING] else 30
1419 flight.handleFault(StallChecker, state.timestamp,
1420 FaultChecker._appendDuring(flight, "Stalled"),
1421 score)
1422 else:
1423 logger.message(state.timestamp,
1424 FaultChecker._appendDuring(flight, "Stalled"))
[11]1425
1426#---------------------------------------------------------------------------------------
1427
1428class PayloadChecker(SimpleFaultChecker):
1429 """Check if the payload matches the specification."""
1430 TOLERANCE=550
[60]1431
1432 @staticmethod
1433 def isZFWFaulty(aircraftZFW, flightZFW):
1434 """Check if the given aircraft's ZFW is outside of the limits."""
1435 return aircraftZFW < (flightZFW - PayloadChecker.TOLERANCE) or \
[332]1436 aircraftZFW > (flightZFW + PayloadChecker.TOLERANCE)
1437
[11]1438 def isCondition(self, flight, aircraft, oldState, state):
1439 """Check if the fault condition holds."""
[192]1440 return not flight.entranceExam and \
[184]1441 flight.stage==const.STAGE_PUSHANDTAXI and \
[60]1442 PayloadChecker.isZFWFaulty(state.zfw, flight.zfw)
[332]1443
[11]1444 def logFault(self, flight, aircraft, logger, oldState, state):
1445 """Log the fault."""
[30]1446 flight.handleNoGo(PayloadChecker, state.timestamp,
1447 "ZFW difference is more than %d kgs" % \
1448 (PayloadChecker.TOLERANCE,),
1449 "ZFW NO GO")
[11]1450
1451#---------------------------------------------------------------------------------------
1452
1453class PitotChecker(PatientFaultChecker):
1454 """Check if pitot heat is on."""
1455 def __init__(self):
1456 """Construct the checker."""
1457 super(PitotChecker, self).__init__(timeout = 3.0)
1458
1459 def isCondition(self, flight, aircraft, oldState, state):
1460 """Check if the fault condition holds."""
[666]1461 return state.groundSpeed>80 and state.pitotHeatOn is False
[332]1462
[11]1463 def logFault(self, flight, aircraft, logger, oldState, state):
1464 """Log the fault."""
1465 score = 2 if flight.stage in [const.STAGE_TAKEOFF, const.STAGE_CLIMB,
1466 const.STAGE_CRUISE, const.STAGE_DESCENT,
1467 const.STAGE_LANDING] else 0
[30]1468 flight.handleFault(PitotChecker, state.timestamp,
1469 FaultChecker._appendDuring(flight, "Pitot heat was off"),
[332]1470 score)
[11]1471
1472#---------------------------------------------------------------------------------------
1473
[598]1474class ReverserLogger(StateChecker):
1475 """Logger for the reverser."""
1476 def check(self, flight, aircraft, logger, oldState, state):
1477 """Log the cruise speed if necessary."""
1478 if oldState is not None and state.reverser != oldState.reverser:
1479 reverser = max(state.reverser)
1480 logger.message(state.timestamp,
1481 "Reverser " + ("unlocked" if reverser else "closed") +
1482 (" - %.0f %s" % (flight.speedFromKnots(state.ias),
1483 flight.getEnglishSpeedUnit())))
1484
1485#---------------------------------------------------------------------------------------
1486
[11]1487class ReverserChecker(SimpleFaultChecker):
[360]1488 """Check if the reverser is not used below the speed prescribed for the
1489 aircraft."""
[11]1490 def isCondition(self, flight, aircraft, oldState, state):
1491 """Check if the fault condition holds."""
1492 return flight.stage in [const.STAGE_DESCENT, const.STAGE_LANDING,
1493 const.STAGE_TAXIAFTERLAND] and \
[443]1494 state.reverser and \
[598]1495 state.ias<aircraft.reverseMinSpeed and max(state.reverser)
[332]1496
[11]1497 def logFault(self, flight, aircraft, logger, oldState, state):
1498 """Log the fault."""
[241]1499 message = "Reverser used below %.0f %s" % \
[598]1500 (flight.speedFromKnots(aircraft.reverseMinSpeed), flight.getEnglishSpeedUnit())
[30]1501 flight.handleFault(ReverserChecker, state.timestamp,
[241]1502 FaultChecker._appendDuring(flight, message),
1503 15)
[11]1504
1505#---------------------------------------------------------------------------------------
1506
[623]1507class SpeedChecker(StateChecker):
1508 """Checker for the ground speed of aircraft.
[539]1509
[623]1510 If, during the PUSHANDTAXI stage the speed exceeds 50 knots, the state is
1511 saved as a provisional takeoff state. If the speed then decreases below 50
1512 knots, or the plane remains on the ground for more than 40 seconds, a taxi
1513 speed error is logged with the highest ground speed detected. This state is
1514 also stored in the flight object as a possible
1515
1516 If the plane becomes airborne within 40 seconds, the stage becomes TAKEOFF,
1517 and the previously saved takeoff state is logged.
1518
1519 During the TAXIAFTERLAND stage, if the speed goes above 50 knots, a fault
1520 is logged based on the actual speed."""
[539]1521 @staticmethod
[349]1522 def logSpeedFault(flight, state, stage = None, updateID = None):
[344]1523 """Log the speed fault."""
1524 message = "Taxi speed over %.0f %s" % \
1525 (flight.speedFromKnots(50), flight.getEnglishSpeedUnit())
[349]1526 if stage is None:
1527 stage = flight.stage
1528 score = FaultChecker._getLinearScore(50, 80, 10, 15, state.groundSpeed)
1529 return flight.handleFault((SpeedChecker, stage), state.timestamp,
1530 FaultChecker._appendDuring(flight, message),
1531 score, updatePrevious = updateID is None,
1532 updateID = updateID)
[344]1533
1534 def __init__(self):
1535 """Initialize the speed checker."""
1536 self._takeoffState = None
1537 self._highestSpeedState = None
1538
1539 def check(self, flight, aircraft, logger, oldState, state):
1540 """Check the state as described above."""
[623]1541 if flight.stage==const.STAGE_PUSHANDTAXI or \
1542 flight.stage==const.STAGE_RTO:
[644]1543 if state.groundSpeed>50 and aircraft.hasStrobeLight and \
1544 state.strobeLightsOn is False:
1545 self.logSpeedFault(flight, state)
1546 else:
1547 self._checkPushAndTaxi(flight, aircraft, state)
[623]1548 elif flight.stage==const.STAGE_TAXIAFTERLAND:
1549 if state.groundSpeed>50:
1550 self.logSpeedFault(flight, state)
1551 else:
1552 self._takeoffState = None
[344]1553
1554 def _checkPushAndTaxi(self, flight, aircraft, state):
1555 """Check the speed during the push and taxi stage."""
1556 if state.groundSpeed>50:
1557 if self._takeoffState is None:
1558 self._takeoffState = state
1559 self._highestSpeedState = state
1560 else:
1561 if state.groundSpeed>self._highestSpeedState.groundSpeed:
1562 self._highestSpeedState = state
1563
[414]1564 if not state.onTheGround:
[344]1565 aircraft.setStage(self._takeoffState, const.STAGE_TAKEOFF)
1566 self._takeoffState = None
[643]1567 elif state.timestamp > (self._takeoffState.timestamp + 60):
[414]1568 flight.setRTOState(self._highestSpeedState)
[344]1569 elif self._takeoffState is not None:
[349]1570 flight.setRTOState(self._highestSpeedState)
[344]1571 self._takeoffState = None
[11]1572
1573#---------------------------------------------------------------------------------------
1574
1575class StrobeLightsChecker(PatientFaultChecker):
1576 """Check if the strobe lights are used properly."""
[1059]1577 def __init__(self):
1578 """Construct the Strobe lights checker."""
1579 super(StrobeLightsChecker, self).__init__(timeout = 5.0)
1580
[11]1581 def isCondition(self, flight, aircraft, oldState, state):
1582 """Check if the fault condition holds."""
[480]1583 return state.strobeLightsOn is not None and \
1584 ((flight.stage==const.STAGE_BOARDING and \
[211]1585 state.strobeLightsOn and state.onTheGround) or \
1586 (flight.stage==const.STAGE_TAKEOFF and \
1587 not state.strobeLightsOn and not state.gearsDown) or \
1588 (flight.stage in [const.STAGE_CLIMB, const.STAGE_CRUISE,
1589 const.STAGE_DESCENT] and \
1590 not state.strobeLightsOn and not state.onTheGround) or \
1591 (flight.stage==const.STAGE_PARKING and \
[480]1592 state.strobeLightsOn and state.onTheGround))
[332]1593
[11]1594 def logFault(self, flight, aircraft, logger, oldState, state):
1595 """Log the fault."""
1596 message = "Strobe lights were %s" % (("on" if state.strobeLightsOn else "off"),)
[30]1597 flight.handleFault(StrobeLightsChecker, state.timestamp,
1598 FaultChecker._appendDuring(flight, message),
1599 1)
[11]1600
1601#---------------------------------------------------------------------------------------
1602
1603class ThrustChecker(SimpleFaultChecker):
1604 """Check if the thrust setting is not too high during takeoff.
1605
1606 FIXME: is this really so general, for all aircraft?"""
1607 def isCondition(self, flight, aircraft, oldState, state):
1608 """Check if the fault condition holds."""
[1043]1609 if flight.stage==const.STAGE_TAKEOFF and state.n1 is not None:
1610 for n1 in state.n1:
1611 if n1 is not None and n1>97:
1612 return True
1613 return False
[332]1614
[11]1615 def logFault(self, flight, aircraft, logger, oldState, state):
1616 """Log the fault."""
[30]1617 flight.handleFault(ThrustChecker, state.timestamp,
1618 FaultChecker._appendDuring(flight,
1619 "Thrust setting was too high (>97%)"),
[405]1620 FaultChecker._getLinearScore(97, 110, 0, 10,
1621 max(state.n1)),
1622 updatePrevious = True)
[11]1623
1624#---------------------------------------------------------------------------------------
1625
1626class VSChecker(SimpleFaultChecker):
1627 """Check if the vertical speed is not too low at certain altitudes"""
1628 BELOW10000 = -5000
1629 BELOW5000 = -2500
1630 BELOW2500 = -1500
1631 BELOW500 = -1000
1632 TOLERANCE = 1.2
1633
1634 def isCondition(self, flight, aircraft, oldState, state):
1635 """Check if the fault condition holds."""
[197]1636 vs = state.smoothedVS
[11]1637 altitude = state.altitude
1638 return vs < -8000 or vs > 8000 or \
1639 (altitude<500 and vs < (VSChecker.BELOW500 *
[13]1640 VSChecker.TOLERANCE)) or \
[11]1641 (altitude<2500 and vs < (VSChecker.BELOW2500 *
[13]1642 VSChecker.TOLERANCE)) or \
[11]1643 (altitude<5000 and vs < (VSChecker.BELOW5000 *
[13]1644 VSChecker.TOLERANCE)) or \
[11]1645 (altitude<10000 and vs < (VSChecker.BELOW10000 *
[13]1646 VSChecker.TOLERANCE))
[332]1647
[11]1648 def logFault(self, flight, aircraft, logger, oldState, state):
1649 """Log the fault."""
[197]1650 vs = state.smoothedVS
[11]1651
1652 message = "Vertical speed was %.0f feet/min" % (vs,)
1653 if vs>-8000 and vs<8000:
1654 message += " at %.0f feet (exceeds company limit)" % (state.altitude,)
1655
1656 score = 10 if vs<-8000 or vs>8000 else 0
1657
[30]1658 flight.handleFault(VSChecker, state.timestamp,
1659 FaultChecker._appendDuring(flight, message),
1660 score)
[11]1661
1662#---------------------------------------------------------------------------------------
Note: See TracBrowser for help on using the repository browser.