source: src/mlx/checks.py@ 1107:5f0d48ebebc3

python3
Last change on this file since 1107:5f0d48ebebc3 was 1107:5f0d48ebebc3, checked in by István Váradi <ivaradi@…>, 8 months ago

When boarding at a taxi-through stand, the turning on of the anti-collision lights cause a phase transition to pushback and start (re #371)

File size: 70.2 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."""
961 def isCondition(self, flight, aircraft, oldState, state):
962 """Check if the fault condition holds."""
[519]963 return (not flight.config.usingFS2Crew or not state.parking or
964 flight.stage!=const.STAGE_TAXIAFTERLAND) and \
[661]965 state.antiCollisionLightsOn is False and \
[211]966 self.isEngineCondition(state)
[11]967
968 def logFault(self, flight, aircraft, logger, oldState, state):
969 """Log the fault."""
[30]970 flight.handleFault(AntiCollisionLightsChecker, state.timestamp,
971 FaultChecker._appendDuring(flight,
[548]972 "Anti-collision lights were %s" %
973 ("on" if state.antiCollisionLightsOn else "off",)),
[30]974 1)
[11]975
[211]976 def isEngineCondition(self, state):
977 """Determine if the engines are in such a state that the lights should
978 be on."""
[263]979 if state.n1 is not None:
[1043]980 for n1 in state.n1:
981 if n1 is not None and n1>5:
982 return True
983 return False
[263]984 elif state.rpm is not None:
985 return max(state.rpm)>0
986 else:
987 return False
[211]988
989#---------------------------------------------------------------------------------------
990
991class TupolevAntiCollisionLightsChecker(AntiCollisionLightsChecker):
992 """Check for the anti-collision light for Tuplev planes."""
[307]993 def isCondition(self, flight, aircraft, oldState, state):
[332]994 """Check if the fault condition holds."""
[307]995 numEnginesRunning = 0
996 for n1 in state.n1:
997 if n1>5: numEnginesRunning += 1
[332]998
[452]999 if flight.stage==const.STAGE_PARKING or \
[548]1000 flight.stage==const.STAGE_TAXIAFTERLAND:
[307]1001 return numEnginesRunning<len(state.n1) \
1002 and state.antiCollisionLightsOn
1003 else:
1004 return numEnginesRunning>1 and not state.antiCollisionLightsOn or \
1005 numEnginesRunning<1 and state.antiCollisionLightsOn
[211]1006
[11]1007#---------------------------------------------------------------------------------------
1008
[631]1009class TupolevLandingLightsChecker(PatientFaultChecker):
1010 """Check if the landing light is not switched on above an IAS of 340 km/h."""
1011 def isCondition(self, flight, aircraft, oldState, state):
1012 """Check if the fault condition holds."""
1013 return state.landingLightsOn and state.ias>(340.0*const.KMPHTOKNOTS)
1014
1015 def logFault(self, flight, aircraft, logger, oldState, state):
1016 """Log the fault."""
1017 flight.handleFault(TupolevLandingLightsChecker, state.timestamp,
1018 "The landing lights were on above an IAS of 340 km/h",
1019 1)
1020
1021#---------------------------------------------------------------------------------------
1022
[11]1023class BankChecker(SimpleFaultChecker):
[263]1024 """Check for the bank is within limits."""
[11]1025 def isCondition(self, flight, aircraft, oldState, state):
1026 """Check if the fault condition holds."""
[912]1027 isXPlane = (flight.aircraftType==const.AIRCRAFT_DH8D or
1028 flight.aircraftType==const.AIRCRAFT_B733) and \
[1073]1029 (flight.fsType==const.SIM_XPLANE12 or
1030 flight.fsType==const.SIM_XPLANE11 or
[912]1031 flight.fsType==const.SIM_XPLANE10 or
1032 flight.fsType==const.SIM_XPLANE9)
[11]1033 if flight.stage==const.STAGE_CRUISE:
[912]1034 bankLimit = 40 if isXPlane else 30
[11]1035 elif flight.stage in [const.STAGE_TAKEOFF, const.STAGE_CLIMB,
1036 const.STAGE_DESCENT, const.STAGE_LANDING]:
[912]1037 bankLimit = 45 if isXPlane else 35
[11]1038 else:
1039 return False
1040
1041 return state.bank>bankLimit or state.bank<-bankLimit
1042
1043 def logFault(self, flight, aircraft, logger, oldState, state):
1044 """Log the fault."""
[420]1045 message = "Bank too steep (%.1f)" % (state.bank,)
[30]1046 flight.handleFault(BankChecker, state.timestamp,
[420]1047 FaultChecker._appendDuring(flight, message),
[30]1048 2)
[11]1049
1050#---------------------------------------------------------------------------------------
1051
1052class FlapsRetractChecker(SimpleFaultChecker):
1053 """Check if the flaps are not retracted too early."""
[13]1054 def __init__(self):
1055 """Construct the flaps checker."""
1056 self._timeStart = None
[332]1057
[11]1058 def isCondition(self, flight, aircraft, oldState, state):
1059 """Check if the fault condition holds.
1060
1061 FIXME: check if this really is the intention (FlapsRetractedMistake.java)"""
[357]1062 if (flight.stage==const.STAGE_TAKEOFF and not state.onTheGround and
1063 aircraft.type!=const.AIRCRAFT_F70) or \
[11]1064 (flight.stage==const.STAGE_LANDING and state.onTheGround):
1065 if self._timeStart is None:
1066 self._timeStart = state.timestamp
1067
1068 if state.flapsSet==0 and state.timestamp<=(self._timeStart+2.0):
1069 return True
[13]1070 else:
1071 self._timeStart = None
[11]1072 return False
1073
1074 def logFault(self, flight, aircraft, logger, oldState, state):
1075 """Log the fault."""
[30]1076 flight.handleFault(FlapsRetractChecker, state.timestamp,
1077 FaultChecker._appendDuring(flight, "Flaps retracted"),
1078 20)
[11]1079
1080#---------------------------------------------------------------------------------------
1081
1082class FlapsSpeedLimitChecker(SimpleFaultChecker):
1083 """Check if the flaps are extended only at the right speeds."""
1084 def isCondition(self, flight, aircraft, oldState, state):
1085 """Check if the fault condition holds."""
1086 speedLimit = aircraft.getFlapsSpeedLimit(state.flapsSet)
[197]1087 return speedLimit is not None and state.smoothedIAS>speedLimit
[11]1088
1089 def logFault(self, flight, aircraft, logger, oldState, state):
1090 """Log the fault."""
[30]1091 flight.handleFault(FlapsSpeedLimitChecker, state.timestamp,
1092 FaultChecker._appendDuring(flight, "Flap speed limit fault"),
1093 5)
[11]1094
1095#---------------------------------------------------------------------------------------
1096
1097class GearsDownChecker(SimpleFaultChecker):
1098 """Check if the gears are down at low altitudes."""
1099 def isCondition(self, flight, aircraft, oldState, state):
1100 """Check if the fault condition holds."""
1101 return state.radioAltitude<10 and not state.gearsDown and \
1102 flight.stage!=const.STAGE_TAKEOFF
1103
1104 def logFault(self, flight, aircraft, logger, oldState, state):
1105 """Log the fault."""
[30]1106 flight.handleNoGo(GearsDownChecker, state.timestamp,
1107 "Gears not down at %.0f feet radio altitude" % \
1108 (state.radioAltitude,),
1109 "GEAR DOWN NO GO")
[11]1110
1111#---------------------------------------------------------------------------------------
1112
1113class GearSpeedLimitChecker(PatientFaultChecker):
1114 """Check if the gears not down at too high a speed."""
1115 def isCondition(self, flight, aircraft, oldState, state):
1116 """Check if the fault condition holds."""
[197]1117 return state.gearsDown and state.smoothedIAS>aircraft.gearSpeedLimit
[11]1118
1119 def logFault(self, flight, aircraft, logger, oldState, state):
1120 """Log the fault."""
[30]1121 flight.handleFault(GearSpeedLimitChecker, state.timestamp,
1122 FaultChecker._appendDuring(flight, "Gear speed limit fault"),
1123 5)
[11]1124
1125#---------------------------------------------------------------------------------------
1126
1127class GLoadChecker(SimpleFaultChecker):
1128 """Check if the G-load does not exceed 2 except during flare."""
1129 def isCondition(self, flight, aircraft, oldState, state):
1130 """Check if the fault condition holds."""
[448]1131 return state.gLoad>2.0 and not state.onTheGround and \
1132 (flight.stage!=const.STAGE_LANDING or state.radioAltitude>=50)
[332]1133
[11]1134 def logFault(self, flight, aircraft, logger, oldState, state):
1135 """Log the fault."""
[30]1136 flight.handleFault(GLoadChecker, state.timestamp,
1137 "G-load was %.2f" % (state.gLoad,),
1138 10)
[11]1139
1140#---------------------------------------------------------------------------------------
1141
1142class LandingLightsChecker(PatientFaultChecker):
1143 """Check if the landing lights are used properly."""
1144 def getTimeout(self, flight, aircraft, oldState, state):
1145 """Get the timeout.
1146
1147 It is the default timeout except for landing and takeoff."""
1148 return 0.0 if flight.stage in [const.STAGE_TAKEOFF,
1149 const.STAGE_LANDING] else self._timeout
1150
1151 def isCondition(self, flight, aircraft, oldState, state):
1152 """Check if the fault condition holds."""
[314]1153 return state.landingLightsOn is not None and \
1154 ((flight.stage==const.STAGE_BOARDING and \
1155 state.landingLightsOn and state.onTheGround) or \
1156 (flight.stage==const.STAGE_TAKEOFF and \
1157 not state.landingLightsOn and not state.onTheGround) or \
1158 (flight.stage in [const.STAGE_CLIMB, const.STAGE_CRUISE,
1159 const.STAGE_DESCENT] and \
1160 state.landingLightsOn and state.altitude>12500) or \
1161 (flight.stage==const.STAGE_LANDING and \
[415]1162 not state.landingLightsOn and state.onTheGround and
[637]1163 state.ias>50.0) or \
[314]1164 (flight.stage==const.STAGE_PARKING and \
1165 state.landingLightsOn and state.onTheGround))
[332]1166
[11]1167 def logFault(self, flight, aircraft, logger, oldState, state):
1168 """Log the fault."""
[375]1169 score = 0 if flight.stage in [const.STAGE_TAKEOFF,
1170 const.STAGE_LANDING] else 1
[335]1171 message = "Landing lights were %s" % \
1172 (("on" if state.landingLightsOn else "off"),)
[30]1173 flight.handleFault(LandingLightsChecker, state.timestamp,
1174 FaultChecker._appendDuring(flight, message),
[332]1175 score)
[11]1176
1177#---------------------------------------------------------------------------------------
1178
[335]1179class TransponderChecker(PatientFaultChecker):
1180 """Check if the transponder is used properly."""
[361]1181 def __init__(self):
1182 """Construct the transponder checker."""
1183 super(TransponderChecker, self).__init__()
1184 self._liftOffTime = None
1185
[335]1186 def isCondition(self, flight, aircraft, oldState, state):
1187 """Check if the fault condition holds."""
[361]1188 if state.onTheGround:
1189 self._liftOffTime = None
1190 elif self._liftOffTime is None:
1191 self._liftOffTime = state.timestamp
1192
[335]1193 return state.xpdrC is not None and \
1194 ((state.xpdrC and flight.stage in
1195 [const.STAGE_BOARDING, const.STAGE_PARKING]) or \
[361]1196 (not state.xpdrC and
1197 (flight.stage in
1198 [const.STAGE_CRUISE, const.STAGE_DESCENT,
[446]1199 const.STAGE_GOAROUND] or \
[518]1200 (flight.stage==const.STAGE_LANDING and
[446]1201 state.groundSpeed>50.0) or \
[361]1202 ((not state.autoXPDR or \
1203 (self._liftOffTime is not None and
1204 state.timestamp > (self._liftOffTime+8))) and \
[518]1205 ((flight.stage==const.STAGE_TAKEOFF and
1206 not state.onTheGround) or flight.stage==const.STAGE_CLIMB))
[361]1207 )
1208 )
1209 )
[335]1210
1211 def logFault(self, flight, aircraft, logger, oldState, state):
1212 """Log the fault."""
1213 score = 0
1214 message = "Transponder was %s" % \
1215 (("mode C" if state.xpdrC else "standby"),)
1216 flight.handleFault(TransponderChecker, state.timestamp,
1217 FaultChecker._appendDuring(flight, message),
1218 score)
1219
1220#---------------------------------------------------------------------------------------
1221
[11]1222class WeightChecker(PatientFaultChecker):
1223 """Base class for checkers that check that some limit is not exceeded."""
1224 def __init__(self, name):
1225 """Construct the checker."""
1226 super(WeightChecker, self).__init__(timeout = 5.0)
1227 self._name = name
1228
1229 def isCondition(self, flight, aircraft, oldState, state):
1230 """Check if the fault condition holds."""
1231 if flight.entranceExam:
1232 return False
1233
1234 limit = self.getLimit(flight, aircraft, state)
1235 if limit is not None:
[183]1236 #if flight.options.compensation is not None:
1237 # limit += flight.options.compensation
[13]1238 return self.getWeight(state)>limit
[11]1239
1240 return False
1241
1242 def logFault(self, flight, aircraft, logger, oldState, state):
1243 """Log the fault."""
[13]1244 mname = "M" + self._name
[30]1245 flight.handleNoGo(self.__class__, state.timestamp,
1246 "%s exceeded: %s is %.0f kg" % \
1247 (mname, self._name, self.getWeight(state)),
1248 "%s NO GO" % (mname,))
[13]1249
1250 def getWeight(self, state):
1251 """Get the weight that is interesting for us."""
1252 return state.grossWeight
[11]1253
1254#---------------------------------------------------------------------------------------
1255
1256class MLWChecker(WeightChecker):
1257 """Checks if the MLW is not exceeded on landing."""
1258 def __init__(self):
1259 """Construct the checker."""
[13]1260 super(MLWChecker, self).__init__("LW")
[11]1261
[13]1262 def getLimit(self, flight, aircraft, state):
[11]1263 """Get the limit if we are in the right state."""
1264 return aircraft.mlw if flight.stage==const.STAGE_LANDING and \
[184]1265 state.onTheGround and \
1266 not flight.entranceExam else None
[11]1267
1268#---------------------------------------------------------------------------------------
1269
1270class MTOWChecker(WeightChecker):
1271 """Checks if the MTOW is not exceeded on landing."""
1272 def __init__(self):
1273 """Construct the checker."""
[13]1274 super(MTOWChecker, self).__init__("TOW")
[11]1275
[13]1276 def getLimit(self, flight, aircraft, state):
[11]1277 """Get the limit if we are in the right state."""
[184]1278 return aircraft.mtow if flight.stage==const.STAGE_TAKEOFF and \
1279 not flight.entranceExam else None
[11]1280
1281#---------------------------------------------------------------------------------------
1282
1283class MZFWChecker(WeightChecker):
[60]1284 """Checks if the MZFW is not exceeded on landing."""
[11]1285 def __init__(self):
1286 """Construct the checker."""
[13]1287 super(MZFWChecker, self).__init__("ZFW")
[11]1288
[13]1289 def getLimit(self, flight, aircraft, state):
[11]1290 """Get the limit if we are in the right state."""
[184]1291 return aircraft.mzfw if not flight.entranceExam else None
[11]1292
[13]1293 def getWeight(self, state):
1294 """Get the weight that is interesting for us."""
1295 return state.zfw
1296
[11]1297#---------------------------------------------------------------------------------------
1298
1299class NavLightsChecker(PatientFaultChecker):
1300 """Check if the navigational lights are used properly."""
[1059]1301 def __init__(self):
1302 """Construct the NAV lights checker."""
1303 super(NavLightsChecker, self).__init__(timeout = 5.0)
1304
[11]1305 def isCondition(self, flight, aircraft, oldState, state):
1306 """Check if the fault condition holds."""
1307 return flight.stage!=const.STAGE_BOARDING and \
1308 flight.stage!=const.STAGE_PARKING and \
[341]1309 state.navLightsOn is False
[332]1310
[11]1311 def logFault(self, flight, aircraft, logger, oldState, state):
1312 """Log the fault."""
[30]1313 flight.handleFault(NavLightsChecker, state.timestamp,
1314 FaultChecker._appendDuring(flight,
1315 "Navigation lights were off"),
1316 1)
[11]1317
1318#---------------------------------------------------------------------------------------
1319
[622]1320class WindSensitiveFaultChecker(FaultChecker):
1321 """A fault checker which checks for a fault condition that might arise due
1322 to suddenly changing winds.
1323
1324 If the condition is detected, its fact is logged, and the winds are also
1325 logged repeatedly until and for a while after the condition has ceased. A
1326 fault is logged only if the condition holds for a certain period of
1327 time."""
1328 # The time the winds were logged last. This is global to avoid duplicate
1329 # logging if several wind-sensitive fault conditions gold
1330 _windsLastLogged = None
1331
1332 # The wind logging interval
1333 _windsLogInterval = 5.0
1334
1335 def __init__(self, faultTimeout = 30.0, afterLoggingTime = 20.0):
1336 """Construct the fault checker."""
1337 self._faultTimeout = faultTimeout
1338 self._afterLoggingTime = afterLoggingTime
1339
1340 self._faultStarted = None
1341 self._faultCeased = None
1342
1343 def check(self, flight, aircraft, logger, oldState, state):
1344 """Check for the condition and do whatever is needed."""
1345 timestamp = state.timestamp
1346 logWinds = False
1347
1348 if self.isCondition(flight, aircraft, oldState, state):
1349 logWinds = True
1350 if self._faultStarted is None:
1351 self._faultStarted = timestamp
1352 self._faultCeased = None
1353 self.logCondition(flight, aircraft, logger, oldState, state,
1354 False)
1355
1356 WindSensitiveFaultChecker._windsLastLogged = None
1357
1358 elif timestamp >= (self._faultStarted + self._faultTimeout):
1359 self.logCondition(flight, aircraft, logger, oldState, state,
1360 True)
1361 self._faultStarted = timestamp
1362
1363 else:
1364 if self._faultStarted is not None and self._faultCeased is None:
1365 self._faultCeased = timestamp
1366 self._faultStarted = None
1367
1368 if self._faultCeased is not None:
1369 if timestamp < (self._faultCeased + self._afterLoggingTime):
1370 logWinds = True
1371 else:
1372 self._faultCeased = None
1373
1374 if logWinds and \
1375 (WindSensitiveFaultChecker._windsLastLogged is None or
1376 timestamp >= (WindSensitiveFaultChecker._windsLastLogged +
1377 self._windsLogInterval)):
1378 logger.message(timestamp, "Winds: %.f knots from %.f" %
1379 (state.windSpeed, state.windDirection))
1380 WindSensitiveFaultChecker._windsLastLogged = timestamp
1381
1382#---------------------------------------------------------------------------------------
1383
1384class OverspeedChecker(WindSensitiveFaultChecker):
[11]1385 """Check if Vne has been exceeded."""
1386 def isCondition(self, flight, aircraft, oldState, state):
1387 """Check if the fault condition holds."""
1388 return state.overspeed
[332]1389
[622]1390 def logCondition(self, flight, aircraft, logger, oldState, state, isFault):
1391 """Log the condition fault."""
1392 if isFault:
1393 flight.handleFault(OverspeedChecker, state.timestamp,
1394 FaultChecker._appendDuring(flight, "Overspeed"),
1395 20)
1396 else:
1397 logger.message(state.timestamp,
1398 FaultChecker._appendDuring(flight, "Overspeed"))
1399
1400#---------------------------------------------------------------------------------------
1401
1402class StallChecker(WindSensitiveFaultChecker):
1403 """Check if stall occured."""
1404 def isCondition(self, flight, aircraft, oldState, state):
1405 """Check if the fault condition holds."""
1406 return flight.stage in [const.STAGE_TAKEOFF, const.STAGE_CLIMB,
1407 const.STAGE_CRUISE, const.STAGE_DESCENT,
1408 const.STAGE_LANDING] and state.stalled
1409
1410 def logCondition(self, flight, aircraft, logger, oldState, state, isFault):
1411 """Log the condition."""
1412 if isFault:
1413 score = 40 if flight.stage in [const.STAGE_TAKEOFF,
1414 const.STAGE_LANDING] else 30
1415 flight.handleFault(StallChecker, state.timestamp,
1416 FaultChecker._appendDuring(flight, "Stalled"),
1417 score)
1418 else:
1419 logger.message(state.timestamp,
1420 FaultChecker._appendDuring(flight, "Stalled"))
[11]1421
1422#---------------------------------------------------------------------------------------
1423
1424class PayloadChecker(SimpleFaultChecker):
1425 """Check if the payload matches the specification."""
1426 TOLERANCE=550
[60]1427
1428 @staticmethod
1429 def isZFWFaulty(aircraftZFW, flightZFW):
1430 """Check if the given aircraft's ZFW is outside of the limits."""
1431 return aircraftZFW < (flightZFW - PayloadChecker.TOLERANCE) or \
[332]1432 aircraftZFW > (flightZFW + PayloadChecker.TOLERANCE)
1433
[11]1434 def isCondition(self, flight, aircraft, oldState, state):
1435 """Check if the fault condition holds."""
[192]1436 return not flight.entranceExam and \
[184]1437 flight.stage==const.STAGE_PUSHANDTAXI and \
[60]1438 PayloadChecker.isZFWFaulty(state.zfw, flight.zfw)
[332]1439
[11]1440 def logFault(self, flight, aircraft, logger, oldState, state):
1441 """Log the fault."""
[30]1442 flight.handleNoGo(PayloadChecker, state.timestamp,
1443 "ZFW difference is more than %d kgs" % \
1444 (PayloadChecker.TOLERANCE,),
1445 "ZFW NO GO")
[11]1446
1447#---------------------------------------------------------------------------------------
1448
1449class PitotChecker(PatientFaultChecker):
1450 """Check if pitot heat is on."""
1451 def __init__(self):
1452 """Construct the checker."""
1453 super(PitotChecker, self).__init__(timeout = 3.0)
1454
1455 def isCondition(self, flight, aircraft, oldState, state):
1456 """Check if the fault condition holds."""
[666]1457 return state.groundSpeed>80 and state.pitotHeatOn is False
[332]1458
[11]1459 def logFault(self, flight, aircraft, logger, oldState, state):
1460 """Log the fault."""
1461 score = 2 if flight.stage in [const.STAGE_TAKEOFF, const.STAGE_CLIMB,
1462 const.STAGE_CRUISE, const.STAGE_DESCENT,
1463 const.STAGE_LANDING] else 0
[30]1464 flight.handleFault(PitotChecker, state.timestamp,
1465 FaultChecker._appendDuring(flight, "Pitot heat was off"),
[332]1466 score)
[11]1467
1468#---------------------------------------------------------------------------------------
1469
[598]1470class ReverserLogger(StateChecker):
1471 """Logger for the reverser."""
1472 def check(self, flight, aircraft, logger, oldState, state):
1473 """Log the cruise speed if necessary."""
1474 if oldState is not None and state.reverser != oldState.reverser:
1475 reverser = max(state.reverser)
1476 logger.message(state.timestamp,
1477 "Reverser " + ("unlocked" if reverser else "closed") +
1478 (" - %.0f %s" % (flight.speedFromKnots(state.ias),
1479 flight.getEnglishSpeedUnit())))
1480
1481#---------------------------------------------------------------------------------------
1482
[11]1483class ReverserChecker(SimpleFaultChecker):
[360]1484 """Check if the reverser is not used below the speed prescribed for the
1485 aircraft."""
[11]1486 def isCondition(self, flight, aircraft, oldState, state):
1487 """Check if the fault condition holds."""
1488 return flight.stage in [const.STAGE_DESCENT, const.STAGE_LANDING,
1489 const.STAGE_TAXIAFTERLAND] and \
[443]1490 state.reverser and \
[598]1491 state.ias<aircraft.reverseMinSpeed and max(state.reverser)
[332]1492
[11]1493 def logFault(self, flight, aircraft, logger, oldState, state):
1494 """Log the fault."""
[241]1495 message = "Reverser used below %.0f %s" % \
[598]1496 (flight.speedFromKnots(aircraft.reverseMinSpeed), flight.getEnglishSpeedUnit())
[30]1497 flight.handleFault(ReverserChecker, state.timestamp,
[241]1498 FaultChecker._appendDuring(flight, message),
1499 15)
[11]1500
1501#---------------------------------------------------------------------------------------
1502
[623]1503class SpeedChecker(StateChecker):
1504 """Checker for the ground speed of aircraft.
[539]1505
[623]1506 If, during the PUSHANDTAXI stage the speed exceeds 50 knots, the state is
1507 saved as a provisional takeoff state. If the speed then decreases below 50
1508 knots, or the plane remains on the ground for more than 40 seconds, a taxi
1509 speed error is logged with the highest ground speed detected. This state is
1510 also stored in the flight object as a possible
1511
1512 If the plane becomes airborne within 40 seconds, the stage becomes TAKEOFF,
1513 and the previously saved takeoff state is logged.
1514
1515 During the TAXIAFTERLAND stage, if the speed goes above 50 knots, a fault
1516 is logged based on the actual speed."""
[539]1517 @staticmethod
[349]1518 def logSpeedFault(flight, state, stage = None, updateID = None):
[344]1519 """Log the speed fault."""
1520 message = "Taxi speed over %.0f %s" % \
1521 (flight.speedFromKnots(50), flight.getEnglishSpeedUnit())
[349]1522 if stage is None:
1523 stage = flight.stage
1524 score = FaultChecker._getLinearScore(50, 80, 10, 15, state.groundSpeed)
1525 return flight.handleFault((SpeedChecker, stage), state.timestamp,
1526 FaultChecker._appendDuring(flight, message),
1527 score, updatePrevious = updateID is None,
1528 updateID = updateID)
[344]1529
1530 def __init__(self):
1531 """Initialize the speed checker."""
1532 self._takeoffState = None
1533 self._highestSpeedState = None
1534
1535 def check(self, flight, aircraft, logger, oldState, state):
1536 """Check the state as described above."""
[623]1537 if flight.stage==const.STAGE_PUSHANDTAXI or \
1538 flight.stage==const.STAGE_RTO:
[644]1539 if state.groundSpeed>50 and aircraft.hasStrobeLight and \
1540 state.strobeLightsOn is False:
1541 self.logSpeedFault(flight, state)
1542 else:
1543 self._checkPushAndTaxi(flight, aircraft, state)
[623]1544 elif flight.stage==const.STAGE_TAXIAFTERLAND:
1545 if state.groundSpeed>50:
1546 self.logSpeedFault(flight, state)
1547 else:
1548 self._takeoffState = None
[344]1549
1550 def _checkPushAndTaxi(self, flight, aircraft, state):
1551 """Check the speed during the push and taxi stage."""
1552 if state.groundSpeed>50:
1553 if self._takeoffState is None:
1554 self._takeoffState = state
1555 self._highestSpeedState = state
1556 else:
1557 if state.groundSpeed>self._highestSpeedState.groundSpeed:
1558 self._highestSpeedState = state
1559
[414]1560 if not state.onTheGround:
[344]1561 aircraft.setStage(self._takeoffState, const.STAGE_TAKEOFF)
1562 self._takeoffState = None
[643]1563 elif state.timestamp > (self._takeoffState.timestamp + 60):
[414]1564 flight.setRTOState(self._highestSpeedState)
[344]1565 elif self._takeoffState is not None:
[349]1566 flight.setRTOState(self._highestSpeedState)
[344]1567 self._takeoffState = None
[11]1568
1569#---------------------------------------------------------------------------------------
1570
1571class StrobeLightsChecker(PatientFaultChecker):
1572 """Check if the strobe lights are used properly."""
[1059]1573 def __init__(self):
1574 """Construct the Strobe lights checker."""
1575 super(StrobeLightsChecker, self).__init__(timeout = 5.0)
1576
[11]1577 def isCondition(self, flight, aircraft, oldState, state):
1578 """Check if the fault condition holds."""
[480]1579 return state.strobeLightsOn is not None and \
1580 ((flight.stage==const.STAGE_BOARDING and \
[211]1581 state.strobeLightsOn and state.onTheGround) or \
1582 (flight.stage==const.STAGE_TAKEOFF and \
1583 not state.strobeLightsOn and not state.gearsDown) or \
1584 (flight.stage in [const.STAGE_CLIMB, const.STAGE_CRUISE,
1585 const.STAGE_DESCENT] and \
1586 not state.strobeLightsOn and not state.onTheGround) or \
1587 (flight.stage==const.STAGE_PARKING and \
[480]1588 state.strobeLightsOn and state.onTheGround))
[332]1589
[11]1590 def logFault(self, flight, aircraft, logger, oldState, state):
1591 """Log the fault."""
1592 message = "Strobe lights were %s" % (("on" if state.strobeLightsOn else "off"),)
[30]1593 flight.handleFault(StrobeLightsChecker, state.timestamp,
1594 FaultChecker._appendDuring(flight, message),
1595 1)
[11]1596
1597#---------------------------------------------------------------------------------------
1598
1599class ThrustChecker(SimpleFaultChecker):
1600 """Check if the thrust setting is not too high during takeoff.
1601
1602 FIXME: is this really so general, for all aircraft?"""
1603 def isCondition(self, flight, aircraft, oldState, state):
1604 """Check if the fault condition holds."""
[1043]1605 if flight.stage==const.STAGE_TAKEOFF and state.n1 is not None:
1606 for n1 in state.n1:
1607 if n1 is not None and n1>97:
1608 return True
1609 return False
[332]1610
[11]1611 def logFault(self, flight, aircraft, logger, oldState, state):
1612 """Log the fault."""
[30]1613 flight.handleFault(ThrustChecker, state.timestamp,
1614 FaultChecker._appendDuring(flight,
1615 "Thrust setting was too high (>97%)"),
[405]1616 FaultChecker._getLinearScore(97, 110, 0, 10,
1617 max(state.n1)),
1618 updatePrevious = True)
[11]1619
1620#---------------------------------------------------------------------------------------
1621
1622class VSChecker(SimpleFaultChecker):
1623 """Check if the vertical speed is not too low at certain altitudes"""
1624 BELOW10000 = -5000
1625 BELOW5000 = -2500
1626 BELOW2500 = -1500
1627 BELOW500 = -1000
1628 TOLERANCE = 1.2
1629
1630 def isCondition(self, flight, aircraft, oldState, state):
1631 """Check if the fault condition holds."""
[197]1632 vs = state.smoothedVS
[11]1633 altitude = state.altitude
1634 return vs < -8000 or vs > 8000 or \
1635 (altitude<500 and vs < (VSChecker.BELOW500 *
[13]1636 VSChecker.TOLERANCE)) or \
[11]1637 (altitude<2500 and vs < (VSChecker.BELOW2500 *
[13]1638 VSChecker.TOLERANCE)) or \
[11]1639 (altitude<5000 and vs < (VSChecker.BELOW5000 *
[13]1640 VSChecker.TOLERANCE)) or \
[11]1641 (altitude<10000 and vs < (VSChecker.BELOW10000 *
[13]1642 VSChecker.TOLERANCE))
[332]1643
[11]1644 def logFault(self, flight, aircraft, logger, oldState, state):
1645 """Log the fault."""
[197]1646 vs = state.smoothedVS
[11]1647
1648 message = "Vertical speed was %.0f feet/min" % (vs,)
1649 if vs>-8000 and vs<8000:
1650 message += " at %.0f feet (exceeds company limit)" % (state.altitude,)
1651
1652 score = 10 if vs<-8000 or vs>8000 else 0
1653
[30]1654 flight.handleFault(VSChecker, state.timestamp,
1655 FaultChecker._appendDuring(flight, message),
1656 score)
[11]1657
1658#---------------------------------------------------------------------------------------
Note: See TracBrowser for help on using the repository browser.