source: src/mlx/checks.py@ 1068:928cd01e06c6

python3 version_0.50.3
Last change on this file since 1068:928cd01e06c6 was 1059:07ffbc5c13f8, checked in by István Váradi <ivaradi@…>, 2 years ago

The navigation and strobe lights checkers have a patience of 5 seconds

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