source: src/mlx/checks.py@ 896:0b4cfaafdd24

Last change on this file since 896:0b4cfaafdd24 was 896:0b4cfaafdd24, checked in by István Váradi <ivaradi@…>, 5 years ago

The ZFW tolerance is reduced to 50kgs (re #332)

File size: 69.5 KB
Line 
1
2import fs
3import const
4import util
5from acars import ACARS
6from sound import startSound
7
8import time
9
10#---------------------------------------------------------------------------------------
11
12## @package mlx.checks
13#
14# The classes that check the state of the aircraft.
15#
16# During the flight the program periodically queries various data from the
17# simulator. This data is returned in instances of the \ref
18# mlx.fs.AircraftState class and passed to the various "checkers",
19# i.e. instances of subclasses of the \ref StateChecker class. These checkers
20# perform various checks to see if the aircraft's parameters are within the
21# expected limits, or some of them just logs something.
22#
23# There are a few special ones, such as \ref StageChecker which computes the
24# transitions from one stage of the flight to the next one. Or \ref ACARSSender
25# which sends the ACARS periodically
26
27#---------------------------------------------------------------------------------------
28
29class StateChecker(object):
30 """Base class for classes the instances of which check the aircraft's state
31 from some aspect.
32
33 As a result of the check they may log something, or notify of some fault, etc."""
34 def check(self, flight, aircraft, logger, oldState, state):
35 """Perform the check and do whatever is needed.
36
37 This default implementation raises a NotImplementedError."""
38 raise NotImplementedError()
39
40#---------------------------------------------------------------------------------------
41
42class StageChecker(StateChecker):
43 """Check the flight stage transitions."""
44 def __init__(self):
45 """Construct the stage checker."""
46 self._flareStarted = False
47
48 def check(self, flight, aircraft, logger, oldState, state):
49 """Check the stage of the aircraft."""
50 stage = flight.stage
51 if stage==None:
52 aircraft.setStage(state, const.STAGE_BOARDING)
53 elif stage==const.STAGE_BOARDING:
54 if not state.parking or \
55 (not state.trickMode and state.groundSpeed>5.0):
56 aircraft.setStage(state, const.STAGE_PUSHANDTAXI)
57 elif stage==const.STAGE_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 = u"%-5s %s" % (logName + ":", frequency)
520 if obs is not None: message += u" (%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 += u" cannot be detected, will not log"
750 else:
751 message += u": " + 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, u"AP heading",
770 u"%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, u"AP altitude",
782 u"%.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 if flight.stage==const.STAGE_CRUISE:
1024 isDH8DXplane = flight.aircraftType==const.AIRCRAFT_DH8D and \
1025 (flight.fsType==const.SIM_XPLANE10 or
1026 flight.fsType==const.SIM_XPLANE9)
1027 bankLimit = 35 if isDH8DXplane else 30
1028 elif flight.stage in [const.STAGE_TAKEOFF, const.STAGE_CLIMB,
1029 const.STAGE_DESCENT, const.STAGE_LANDING]:
1030 bankLimit = 35
1031 else:
1032 return False
1033
1034 return state.bank>bankLimit or state.bank<-bankLimit
1035
1036 def logFault(self, flight, aircraft, logger, oldState, state):
1037 """Log the fault."""
1038 message = "Bank too steep (%.1f)" % (state.bank,)
1039 flight.handleFault(BankChecker, state.timestamp,
1040 FaultChecker._appendDuring(flight, message),
1041 2)
1042
1043#---------------------------------------------------------------------------------------
1044
1045class FlapsRetractChecker(SimpleFaultChecker):
1046 """Check if the flaps are not retracted too early."""
1047 def __init__(self):
1048 """Construct the flaps checker."""
1049 self._timeStart = None
1050
1051 def isCondition(self, flight, aircraft, oldState, state):
1052 """Check if the fault condition holds.
1053
1054 FIXME: check if this really is the intention (FlapsRetractedMistake.java)"""
1055 if (flight.stage==const.STAGE_TAKEOFF and not state.onTheGround and
1056 aircraft.type!=const.AIRCRAFT_F70) or \
1057 (flight.stage==const.STAGE_LANDING and state.onTheGround):
1058 if self._timeStart is None:
1059 self._timeStart = state.timestamp
1060
1061 if state.flapsSet==0 and state.timestamp<=(self._timeStart+2.0):
1062 return True
1063 else:
1064 self._timeStart = None
1065 return False
1066
1067 def logFault(self, flight, aircraft, logger, oldState, state):
1068 """Log the fault."""
1069 flight.handleFault(FlapsRetractChecker, state.timestamp,
1070 FaultChecker._appendDuring(flight, "Flaps retracted"),
1071 20)
1072
1073#---------------------------------------------------------------------------------------
1074
1075class FlapsSpeedLimitChecker(SimpleFaultChecker):
1076 """Check if the flaps are extended only at the right speeds."""
1077 def isCondition(self, flight, aircraft, oldState, state):
1078 """Check if the fault condition holds."""
1079 speedLimit = aircraft.getFlapsSpeedLimit(state.flapsSet)
1080 return speedLimit is not None and state.smoothedIAS>speedLimit
1081
1082 def logFault(self, flight, aircraft, logger, oldState, state):
1083 """Log the fault."""
1084 flight.handleFault(FlapsSpeedLimitChecker, state.timestamp,
1085 FaultChecker._appendDuring(flight, "Flap speed limit fault"),
1086 5)
1087
1088#---------------------------------------------------------------------------------------
1089
1090class GearsDownChecker(SimpleFaultChecker):
1091 """Check if the gears are down at low altitudes."""
1092 def isCondition(self, flight, aircraft, oldState, state):
1093 """Check if the fault condition holds."""
1094 return state.radioAltitude<10 and not state.gearsDown and \
1095 flight.stage!=const.STAGE_TAKEOFF
1096
1097 def logFault(self, flight, aircraft, logger, oldState, state):
1098 """Log the fault."""
1099 flight.handleNoGo(GearsDownChecker, state.timestamp,
1100 "Gears not down at %.0f feet radio altitude" % \
1101 (state.radioAltitude,),
1102 "GEAR DOWN NO GO")
1103
1104#---------------------------------------------------------------------------------------
1105
1106class GearSpeedLimitChecker(PatientFaultChecker):
1107 """Check if the gears not down at too high a speed."""
1108 def isCondition(self, flight, aircraft, oldState, state):
1109 """Check if the fault condition holds."""
1110 return state.gearsDown and state.smoothedIAS>aircraft.gearSpeedLimit
1111
1112 def logFault(self, flight, aircraft, logger, oldState, state):
1113 """Log the fault."""
1114 flight.handleFault(GearSpeedLimitChecker, state.timestamp,
1115 FaultChecker._appendDuring(flight, "Gear speed limit fault"),
1116 5)
1117
1118#---------------------------------------------------------------------------------------
1119
1120class GLoadChecker(SimpleFaultChecker):
1121 """Check if the G-load does not exceed 2 except during flare."""
1122 def isCondition(self, flight, aircraft, oldState, state):
1123 """Check if the fault condition holds."""
1124 return state.gLoad>2.0 and not state.onTheGround and \
1125 (flight.stage!=const.STAGE_LANDING or state.radioAltitude>=50)
1126
1127 def logFault(self, flight, aircraft, logger, oldState, state):
1128 """Log the fault."""
1129 flight.handleFault(GLoadChecker, state.timestamp,
1130 "G-load was %.2f" % (state.gLoad,),
1131 10)
1132
1133#---------------------------------------------------------------------------------------
1134
1135class LandingLightsChecker(PatientFaultChecker):
1136 """Check if the landing lights are used properly."""
1137 def getTimeout(self, flight, aircraft, oldState, state):
1138 """Get the timeout.
1139
1140 It is the default timeout except for landing and takeoff."""
1141 return 0.0 if flight.stage in [const.STAGE_TAKEOFF,
1142 const.STAGE_LANDING] else self._timeout
1143
1144 def isCondition(self, flight, aircraft, oldState, state):
1145 """Check if the fault condition holds."""
1146 return state.landingLightsOn is not None and \
1147 ((flight.stage==const.STAGE_BOARDING and \
1148 state.landingLightsOn and state.onTheGround) or \
1149 (flight.stage==const.STAGE_TAKEOFF and \
1150 not state.landingLightsOn and not state.onTheGround) or \
1151 (flight.stage in [const.STAGE_CLIMB, const.STAGE_CRUISE,
1152 const.STAGE_DESCENT] and \
1153 state.landingLightsOn and state.altitude>12500) or \
1154 (flight.stage==const.STAGE_LANDING and \
1155 not state.landingLightsOn and state.onTheGround and
1156 state.ias>50.0) or \
1157 (flight.stage==const.STAGE_PARKING and \
1158 state.landingLightsOn and state.onTheGround))
1159
1160 def logFault(self, flight, aircraft, logger, oldState, state):
1161 """Log the fault."""
1162 score = 0 if flight.stage in [const.STAGE_TAKEOFF,
1163 const.STAGE_LANDING] else 1
1164 message = "Landing lights were %s" % \
1165 (("on" if state.landingLightsOn else "off"),)
1166 flight.handleFault(LandingLightsChecker, state.timestamp,
1167 FaultChecker._appendDuring(flight, message),
1168 score)
1169
1170#---------------------------------------------------------------------------------------
1171
1172class TransponderChecker(PatientFaultChecker):
1173 """Check if the transponder is used properly."""
1174 def __init__(self):
1175 """Construct the transponder checker."""
1176 super(TransponderChecker, self).__init__()
1177 self._liftOffTime = None
1178
1179 def isCondition(self, flight, aircraft, oldState, state):
1180 """Check if the fault condition holds."""
1181 if state.onTheGround:
1182 self._liftOffTime = None
1183 elif self._liftOffTime is None:
1184 self._liftOffTime = state.timestamp
1185
1186 return state.xpdrC is not None and \
1187 ((state.xpdrC and flight.stage in
1188 [const.STAGE_BOARDING, const.STAGE_PARKING]) or \
1189 (not state.xpdrC and
1190 (flight.stage in
1191 [const.STAGE_CRUISE, const.STAGE_DESCENT,
1192 const.STAGE_GOAROUND] or \
1193 (flight.stage==const.STAGE_LANDING and
1194 state.groundSpeed>50.0) or \
1195 ((not state.autoXPDR or \
1196 (self._liftOffTime is not None and
1197 state.timestamp > (self._liftOffTime+8))) and \
1198 ((flight.stage==const.STAGE_TAKEOFF and
1199 not state.onTheGround) or flight.stage==const.STAGE_CLIMB))
1200 )
1201 )
1202 )
1203
1204 def logFault(self, flight, aircraft, logger, oldState, state):
1205 """Log the fault."""
1206 score = 0
1207 message = "Transponder was %s" % \
1208 (("mode C" if state.xpdrC else "standby"),)
1209 flight.handleFault(TransponderChecker, state.timestamp,
1210 FaultChecker._appendDuring(flight, message),
1211 score)
1212
1213#---------------------------------------------------------------------------------------
1214
1215class WeightChecker(PatientFaultChecker):
1216 """Base class for checkers that check that some limit is not exceeded."""
1217 def __init__(self, name):
1218 """Construct the checker."""
1219 super(WeightChecker, self).__init__(timeout = 5.0)
1220 self._name = name
1221
1222 def isCondition(self, flight, aircraft, oldState, state):
1223 """Check if the fault condition holds."""
1224 if flight.entranceExam:
1225 return False
1226
1227 limit = self.getLimit(flight, aircraft, state)
1228 if limit is not None:
1229 #if flight.options.compensation is not None:
1230 # limit += flight.options.compensation
1231 return self.getWeight(state)>limit
1232
1233 return False
1234
1235 def logFault(self, flight, aircraft, logger, oldState, state):
1236 """Log the fault."""
1237 mname = "M" + self._name
1238 flight.handleNoGo(self.__class__, state.timestamp,
1239 "%s exceeded: %s is %.0f kg" % \
1240 (mname, self._name, self.getWeight(state)),
1241 "%s NO GO" % (mname,))
1242
1243 def getWeight(self, state):
1244 """Get the weight that is interesting for us."""
1245 return state.grossWeight
1246
1247#---------------------------------------------------------------------------------------
1248
1249class MLWChecker(WeightChecker):
1250 """Checks if the MLW is not exceeded on landing."""
1251 def __init__(self):
1252 """Construct the checker."""
1253 super(MLWChecker, self).__init__("LW")
1254
1255 def getLimit(self, flight, aircraft, state):
1256 """Get the limit if we are in the right state."""
1257 return aircraft.mlw if flight.stage==const.STAGE_LANDING and \
1258 state.onTheGround and \
1259 not flight.entranceExam else None
1260
1261#---------------------------------------------------------------------------------------
1262
1263class MTOWChecker(WeightChecker):
1264 """Checks if the MTOW is not exceeded on landing."""
1265 def __init__(self):
1266 """Construct the checker."""
1267 super(MTOWChecker, self).__init__("TOW")
1268
1269 def getLimit(self, flight, aircraft, state):
1270 """Get the limit if we are in the right state."""
1271 return aircraft.mtow if flight.stage==const.STAGE_TAKEOFF and \
1272 not flight.entranceExam else None
1273
1274#---------------------------------------------------------------------------------------
1275
1276class MZFWChecker(WeightChecker):
1277 """Checks if the MZFW is not exceeded on landing."""
1278 def __init__(self):
1279 """Construct the checker."""
1280 super(MZFWChecker, self).__init__("ZFW")
1281
1282 def getLimit(self, flight, aircraft, state):
1283 """Get the limit if we are in the right state."""
1284 return aircraft.mzfw if not flight.entranceExam else None
1285
1286 def getWeight(self, state):
1287 """Get the weight that is interesting for us."""
1288 return state.zfw
1289
1290#---------------------------------------------------------------------------------------
1291
1292class NavLightsChecker(PatientFaultChecker):
1293 """Check if the navigational lights are used properly."""
1294 def isCondition(self, flight, aircraft, oldState, state):
1295 """Check if the fault condition holds."""
1296 return flight.stage!=const.STAGE_BOARDING and \
1297 flight.stage!=const.STAGE_PARKING and \
1298 state.navLightsOn is False
1299
1300 def logFault(self, flight, aircraft, logger, oldState, state):
1301 """Log the fault."""
1302 flight.handleFault(NavLightsChecker, state.timestamp,
1303 FaultChecker._appendDuring(flight,
1304 "Navigation lights were off"),
1305 1)
1306
1307#---------------------------------------------------------------------------------------
1308
1309class WindSensitiveFaultChecker(FaultChecker):
1310 """A fault checker which checks for a fault condition that might arise due
1311 to suddenly changing winds.
1312
1313 If the condition is detected, its fact is logged, and the winds are also
1314 logged repeatedly until and for a while after the condition has ceased. A
1315 fault is logged only if the condition holds for a certain period of
1316 time."""
1317 # The time the winds were logged last. This is global to avoid duplicate
1318 # logging if several wind-sensitive fault conditions gold
1319 _windsLastLogged = None
1320
1321 # The wind logging interval
1322 _windsLogInterval = 5.0
1323
1324 def __init__(self, faultTimeout = 30.0, afterLoggingTime = 20.0):
1325 """Construct the fault checker."""
1326 self._faultTimeout = faultTimeout
1327 self._afterLoggingTime = afterLoggingTime
1328
1329 self._faultStarted = None
1330 self._faultCeased = None
1331
1332 def check(self, flight, aircraft, logger, oldState, state):
1333 """Check for the condition and do whatever is needed."""
1334 timestamp = state.timestamp
1335 logWinds = False
1336
1337 if self.isCondition(flight, aircraft, oldState, state):
1338 logWinds = True
1339 if self._faultStarted is None:
1340 self._faultStarted = timestamp
1341 self._faultCeased = None
1342 self.logCondition(flight, aircraft, logger, oldState, state,
1343 False)
1344
1345 WindSensitiveFaultChecker._windsLastLogged = None
1346
1347 elif timestamp >= (self._faultStarted + self._faultTimeout):
1348 self.logCondition(flight, aircraft, logger, oldState, state,
1349 True)
1350 self._faultStarted = timestamp
1351
1352 else:
1353 if self._faultStarted is not None and self._faultCeased is None:
1354 self._faultCeased = timestamp
1355 self._faultStarted = None
1356
1357 if self._faultCeased is not None:
1358 if timestamp < (self._faultCeased + self._afterLoggingTime):
1359 logWinds = True
1360 else:
1361 self._faultCeased = None
1362
1363 if logWinds and \
1364 (WindSensitiveFaultChecker._windsLastLogged is None or
1365 timestamp >= (WindSensitiveFaultChecker._windsLastLogged +
1366 self._windsLogInterval)):
1367 logger.message(timestamp, "Winds: %.f knots from %.f" %
1368 (state.windSpeed, state.windDirection))
1369 WindSensitiveFaultChecker._windsLastLogged = timestamp
1370
1371#---------------------------------------------------------------------------------------
1372
1373class OverspeedChecker(WindSensitiveFaultChecker):
1374 """Check if Vne has been exceeded."""
1375 def isCondition(self, flight, aircraft, oldState, state):
1376 """Check if the fault condition holds."""
1377 return state.overspeed
1378
1379 def logCondition(self, flight, aircraft, logger, oldState, state, isFault):
1380 """Log the condition fault."""
1381 if isFault:
1382 flight.handleFault(OverspeedChecker, state.timestamp,
1383 FaultChecker._appendDuring(flight, "Overspeed"),
1384 20)
1385 else:
1386 logger.message(state.timestamp,
1387 FaultChecker._appendDuring(flight, "Overspeed"))
1388
1389#---------------------------------------------------------------------------------------
1390
1391class StallChecker(WindSensitiveFaultChecker):
1392 """Check if stall occured."""
1393 def isCondition(self, flight, aircraft, oldState, state):
1394 """Check if the fault condition holds."""
1395 return flight.stage in [const.STAGE_TAKEOFF, const.STAGE_CLIMB,
1396 const.STAGE_CRUISE, const.STAGE_DESCENT,
1397 const.STAGE_LANDING] and state.stalled
1398
1399 def logCondition(self, flight, aircraft, logger, oldState, state, isFault):
1400 """Log the condition."""
1401 if isFault:
1402 score = 40 if flight.stage in [const.STAGE_TAKEOFF,
1403 const.STAGE_LANDING] else 30
1404 flight.handleFault(StallChecker, state.timestamp,
1405 FaultChecker._appendDuring(flight, "Stalled"),
1406 score)
1407 else:
1408 logger.message(state.timestamp,
1409 FaultChecker._appendDuring(flight, "Stalled"))
1410
1411#---------------------------------------------------------------------------------------
1412
1413class PayloadChecker(SimpleFaultChecker):
1414 """Check if the payload matches the specification."""
1415 TOLERANCE=50
1416
1417 @staticmethod
1418 def isZFWFaulty(aircraftZFW, flightZFW):
1419 """Check if the given aircraft's ZFW is outside of the limits."""
1420 return aircraftZFW < (flightZFW - PayloadChecker.TOLERANCE) or \
1421 aircraftZFW > (flightZFW + PayloadChecker.TOLERANCE)
1422
1423 def isCondition(self, flight, aircraft, oldState, state):
1424 """Check if the fault condition holds."""
1425 return not flight.entranceExam and \
1426 flight.stage==const.STAGE_PUSHANDTAXI and \
1427 PayloadChecker.isZFWFaulty(state.zfw, flight.zfw)
1428
1429 def logFault(self, flight, aircraft, logger, oldState, state):
1430 """Log the fault."""
1431 flight.handleNoGo(PayloadChecker, state.timestamp,
1432 "ZFW difference is more than %d kgs" % \
1433 (PayloadChecker.TOLERANCE,),
1434 "ZFW NO GO")
1435
1436#---------------------------------------------------------------------------------------
1437
1438class PitotChecker(PatientFaultChecker):
1439 """Check if pitot heat is on."""
1440 def __init__(self):
1441 """Construct the checker."""
1442 super(PitotChecker, self).__init__(timeout = 3.0)
1443
1444 def isCondition(self, flight, aircraft, oldState, state):
1445 """Check if the fault condition holds."""
1446 return state.groundSpeed>80 and state.pitotHeatOn is False
1447
1448 def logFault(self, flight, aircraft, logger, oldState, state):
1449 """Log the fault."""
1450 score = 2 if flight.stage in [const.STAGE_TAKEOFF, const.STAGE_CLIMB,
1451 const.STAGE_CRUISE, const.STAGE_DESCENT,
1452 const.STAGE_LANDING] else 0
1453 flight.handleFault(PitotChecker, state.timestamp,
1454 FaultChecker._appendDuring(flight, "Pitot heat was off"),
1455 score)
1456
1457#---------------------------------------------------------------------------------------
1458
1459class ReverserLogger(StateChecker):
1460 """Logger for the reverser."""
1461 def check(self, flight, aircraft, logger, oldState, state):
1462 """Log the cruise speed if necessary."""
1463 if oldState is not None and state.reverser != oldState.reverser:
1464 reverser = max(state.reverser)
1465 logger.message(state.timestamp,
1466 "Reverser " + ("unlocked" if reverser else "closed") +
1467 (" - %.0f %s" % (flight.speedFromKnots(state.ias),
1468 flight.getEnglishSpeedUnit())))
1469
1470#---------------------------------------------------------------------------------------
1471
1472class ReverserChecker(SimpleFaultChecker):
1473 """Check if the reverser is not used below the speed prescribed for the
1474 aircraft."""
1475 def isCondition(self, flight, aircraft, oldState, state):
1476 """Check if the fault condition holds."""
1477 return flight.stage in [const.STAGE_DESCENT, const.STAGE_LANDING,
1478 const.STAGE_TAXIAFTERLAND] and \
1479 state.reverser and \
1480 state.ias<aircraft.reverseMinSpeed and max(state.reverser)
1481
1482 def logFault(self, flight, aircraft, logger, oldState, state):
1483 """Log the fault."""
1484 message = "Reverser used below %.0f %s" % \
1485 (flight.speedFromKnots(aircraft.reverseMinSpeed), flight.getEnglishSpeedUnit())
1486 flight.handleFault(ReverserChecker, state.timestamp,
1487 FaultChecker._appendDuring(flight, message),
1488 15)
1489
1490#---------------------------------------------------------------------------------------
1491
1492class SpeedChecker(StateChecker):
1493 """Checker for the ground speed of aircraft.
1494
1495 If, during the PUSHANDTAXI stage the speed exceeds 50 knots, the state is
1496 saved as a provisional takeoff state. If the speed then decreases below 50
1497 knots, or the plane remains on the ground for more than 40 seconds, a taxi
1498 speed error is logged with the highest ground speed detected. This state is
1499 also stored in the flight object as a possible
1500
1501 If the plane becomes airborne within 40 seconds, the stage becomes TAKEOFF,
1502 and the previously saved takeoff state is logged.
1503
1504 During the TAXIAFTERLAND stage, if the speed goes above 50 knots, a fault
1505 is logged based on the actual speed."""
1506 @staticmethod
1507 def logSpeedFault(flight, state, stage = None, updateID = None):
1508 """Log the speed fault."""
1509 message = "Taxi speed over %.0f %s" % \
1510 (flight.speedFromKnots(50), flight.getEnglishSpeedUnit())
1511 if stage is None:
1512 stage = flight.stage
1513 score = FaultChecker._getLinearScore(50, 80, 10, 15, state.groundSpeed)
1514 return flight.handleFault((SpeedChecker, stage), state.timestamp,
1515 FaultChecker._appendDuring(flight, message),
1516 score, updatePrevious = updateID is None,
1517 updateID = updateID)
1518
1519 def __init__(self):
1520 """Initialize the speed checker."""
1521 self._takeoffState = None
1522 self._highestSpeedState = None
1523
1524 def check(self, flight, aircraft, logger, oldState, state):
1525 """Check the state as described above."""
1526 if flight.stage==const.STAGE_PUSHANDTAXI or \
1527 flight.stage==const.STAGE_RTO:
1528 if state.groundSpeed>50 and aircraft.hasStrobeLight and \
1529 state.strobeLightsOn is False:
1530 self.logSpeedFault(flight, state)
1531 else:
1532 self._checkPushAndTaxi(flight, aircraft, state)
1533 elif flight.stage==const.STAGE_TAXIAFTERLAND:
1534 if state.groundSpeed>50:
1535 self.logSpeedFault(flight, state)
1536 else:
1537 self._takeoffState = None
1538
1539 def _checkPushAndTaxi(self, flight, aircraft, state):
1540 """Check the speed during the push and taxi stage."""
1541 if state.groundSpeed>50:
1542 if self._takeoffState is None:
1543 self._takeoffState = state
1544 self._highestSpeedState = state
1545 else:
1546 if state.groundSpeed>self._highestSpeedState.groundSpeed:
1547 self._highestSpeedState = state
1548
1549 if not state.onTheGround:
1550 aircraft.setStage(self._takeoffState, const.STAGE_TAKEOFF)
1551 self._takeoffState = None
1552 elif state.timestamp > (self._takeoffState.timestamp + 60):
1553 flight.setRTOState(self._highestSpeedState)
1554 elif self._takeoffState is not None:
1555 flight.setRTOState(self._highestSpeedState)
1556 self._takeoffState = None
1557
1558#---------------------------------------------------------------------------------------
1559
1560class StrobeLightsChecker(PatientFaultChecker):
1561 """Check if the strobe lights are used properly."""
1562 def isCondition(self, flight, aircraft, oldState, state):
1563 """Check if the fault condition holds."""
1564 return state.strobeLightsOn is not None and \
1565 ((flight.stage==const.STAGE_BOARDING and \
1566 state.strobeLightsOn and state.onTheGround) or \
1567 (flight.stage==const.STAGE_TAKEOFF and \
1568 not state.strobeLightsOn and not state.gearsDown) or \
1569 (flight.stage in [const.STAGE_CLIMB, const.STAGE_CRUISE,
1570 const.STAGE_DESCENT] and \
1571 not state.strobeLightsOn and not state.onTheGround) or \
1572 (flight.stage==const.STAGE_PARKING and \
1573 state.strobeLightsOn and state.onTheGround))
1574
1575 def logFault(self, flight, aircraft, logger, oldState, state):
1576 """Log the fault."""
1577 message = "Strobe lights were %s" % (("on" if state.strobeLightsOn else "off"),)
1578 flight.handleFault(StrobeLightsChecker, state.timestamp,
1579 FaultChecker._appendDuring(flight, message),
1580 1)
1581
1582#---------------------------------------------------------------------------------------
1583
1584class ThrustChecker(SimpleFaultChecker):
1585 """Check if the thrust setting is not too high during takeoff.
1586
1587 FIXME: is this really so general, for all aircraft?"""
1588 def isCondition(self, flight, aircraft, oldState, state):
1589 """Check if the fault condition holds."""
1590 return flight.stage==const.STAGE_TAKEOFF and \
1591 state.n1 is not None and max(state.n1)>97
1592
1593 def logFault(self, flight, aircraft, logger, oldState, state):
1594 """Log the fault."""
1595 flight.handleFault(ThrustChecker, state.timestamp,
1596 FaultChecker._appendDuring(flight,
1597 "Thrust setting was too high (>97%)"),
1598 FaultChecker._getLinearScore(97, 110, 0, 10,
1599 max(state.n1)),
1600 updatePrevious = True)
1601
1602#---------------------------------------------------------------------------------------
1603
1604class VSChecker(SimpleFaultChecker):
1605 """Check if the vertical speed is not too low at certain altitudes"""
1606 BELOW10000 = -5000
1607 BELOW5000 = -2500
1608 BELOW2500 = -1500
1609 BELOW500 = -1000
1610 TOLERANCE = 1.2
1611
1612 def isCondition(self, flight, aircraft, oldState, state):
1613 """Check if the fault condition holds."""
1614 vs = state.smoothedVS
1615 altitude = state.altitude
1616 return vs < -8000 or vs > 8000 or \
1617 (altitude<500 and vs < (VSChecker.BELOW500 *
1618 VSChecker.TOLERANCE)) or \
1619 (altitude<2500 and vs < (VSChecker.BELOW2500 *
1620 VSChecker.TOLERANCE)) or \
1621 (altitude<5000 and vs < (VSChecker.BELOW5000 *
1622 VSChecker.TOLERANCE)) or \
1623 (altitude<10000 and vs < (VSChecker.BELOW10000 *
1624 VSChecker.TOLERANCE))
1625
1626 def logFault(self, flight, aircraft, logger, oldState, state):
1627 """Log the fault."""
1628 vs = state.smoothedVS
1629
1630 message = "Vertical speed was %.0f feet/min" % (vs,)
1631 if vs>-8000 and vs<8000:
1632 message += " at %.0f feet (exceeds company limit)" % (state.altitude,)
1633
1634 score = 10 if vs<-8000 or vs>8000 else 0
1635
1636 flight.handleFault(VSChecker, state.timestamp,
1637 FaultChecker._appendDuring(flight, message),
1638 score)
1639
1640#---------------------------------------------------------------------------------------
Note: See TracBrowser for help on using the repository browser.