source: src/mlx/checks.py@ 985:f5569c17e934

python3
Last change on this file since 985:f5569c17e934 was 919:2ce8ca39525b, checked in by István Váradi <ivaradi@…>, 6 years ago

Ran 2to3

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