source: src/mlx/checks.py@ 1056:5656e1ae9624

python3
Last change on this file since 1056:5656e1ae9624 was 1043:3629e16455ef, checked in by István Váradi <ivaradi@…>, 3 years ago

Unreadable N1 values are handled better (re #359)

File size: 69.8 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 isCondition(self, flight, aircraft, oldState, state):
1300 """Check if the fault condition holds."""
1301 return flight.stage!=const.STAGE_BOARDING and \
1302 flight.stage!=const.STAGE_PARKING and \
1303 state.navLightsOn is False
1304
1305 def logFault(self, flight, aircraft, logger, oldState, state):
1306 """Log the fault."""
1307 flight.handleFault(NavLightsChecker, state.timestamp,
1308 FaultChecker._appendDuring(flight,
1309 "Navigation lights were off"),
1310 1)
1311
1312#---------------------------------------------------------------------------------------
1313
1314class WindSensitiveFaultChecker(FaultChecker):
1315 """A fault checker which checks for a fault condition that might arise due
1316 to suddenly changing winds.
1317
1318 If the condition is detected, its fact is logged, and the winds are also
1319 logged repeatedly until and for a while after the condition has ceased. A
1320 fault is logged only if the condition holds for a certain period of
1321 time."""
1322 # The time the winds were logged last. This is global to avoid duplicate
1323 # logging if several wind-sensitive fault conditions gold
1324 _windsLastLogged = None
1325
1326 # The wind logging interval
1327 _windsLogInterval = 5.0
1328
1329 def __init__(self, faultTimeout = 30.0, afterLoggingTime = 20.0):
1330 """Construct the fault checker."""
1331 self._faultTimeout = faultTimeout
1332 self._afterLoggingTime = afterLoggingTime
1333
1334 self._faultStarted = None
1335 self._faultCeased = None
1336
1337 def check(self, flight, aircraft, logger, oldState, state):
1338 """Check for the condition and do whatever is needed."""
1339 timestamp = state.timestamp
1340 logWinds = False
1341
1342 if self.isCondition(flight, aircraft, oldState, state):
1343 logWinds = True
1344 if self._faultStarted is None:
1345 self._faultStarted = timestamp
1346 self._faultCeased = None
1347 self.logCondition(flight, aircraft, logger, oldState, state,
1348 False)
1349
1350 WindSensitiveFaultChecker._windsLastLogged = None
1351
1352 elif timestamp >= (self._faultStarted + self._faultTimeout):
1353 self.logCondition(flight, aircraft, logger, oldState, state,
1354 True)
1355 self._faultStarted = timestamp
1356
1357 else:
1358 if self._faultStarted is not None and self._faultCeased is None:
1359 self._faultCeased = timestamp
1360 self._faultStarted = None
1361
1362 if self._faultCeased is not None:
1363 if timestamp < (self._faultCeased + self._afterLoggingTime):
1364 logWinds = True
1365 else:
1366 self._faultCeased = None
1367
1368 if logWinds and \
1369 (WindSensitiveFaultChecker._windsLastLogged is None or
1370 timestamp >= (WindSensitiveFaultChecker._windsLastLogged +
1371 self._windsLogInterval)):
1372 logger.message(timestamp, "Winds: %.f knots from %.f" %
1373 (state.windSpeed, state.windDirection))
1374 WindSensitiveFaultChecker._windsLastLogged = timestamp
1375
1376#---------------------------------------------------------------------------------------
1377
1378class OverspeedChecker(WindSensitiveFaultChecker):
1379 """Check if Vne has been exceeded."""
1380 def isCondition(self, flight, aircraft, oldState, state):
1381 """Check if the fault condition holds."""
1382 return state.overspeed
1383
1384 def logCondition(self, flight, aircraft, logger, oldState, state, isFault):
1385 """Log the condition fault."""
1386 if isFault:
1387 flight.handleFault(OverspeedChecker, state.timestamp,
1388 FaultChecker._appendDuring(flight, "Overspeed"),
1389 20)
1390 else:
1391 logger.message(state.timestamp,
1392 FaultChecker._appendDuring(flight, "Overspeed"))
1393
1394#---------------------------------------------------------------------------------------
1395
1396class StallChecker(WindSensitiveFaultChecker):
1397 """Check if stall occured."""
1398 def isCondition(self, flight, aircraft, oldState, state):
1399 """Check if the fault condition holds."""
1400 return flight.stage in [const.STAGE_TAKEOFF, const.STAGE_CLIMB,
1401 const.STAGE_CRUISE, const.STAGE_DESCENT,
1402 const.STAGE_LANDING] and state.stalled
1403
1404 def logCondition(self, flight, aircraft, logger, oldState, state, isFault):
1405 """Log the condition."""
1406 if isFault:
1407 score = 40 if flight.stage in [const.STAGE_TAKEOFF,
1408 const.STAGE_LANDING] else 30
1409 flight.handleFault(StallChecker, state.timestamp,
1410 FaultChecker._appendDuring(flight, "Stalled"),
1411 score)
1412 else:
1413 logger.message(state.timestamp,
1414 FaultChecker._appendDuring(flight, "Stalled"))
1415
1416#---------------------------------------------------------------------------------------
1417
1418class PayloadChecker(SimpleFaultChecker):
1419 """Check if the payload matches the specification."""
1420 TOLERANCE=550
1421
1422 @staticmethod
1423 def isZFWFaulty(aircraftZFW, flightZFW):
1424 """Check if the given aircraft's ZFW is outside of the limits."""
1425 return aircraftZFW < (flightZFW - PayloadChecker.TOLERANCE) or \
1426 aircraftZFW > (flightZFW + PayloadChecker.TOLERANCE)
1427
1428 def isCondition(self, flight, aircraft, oldState, state):
1429 """Check if the fault condition holds."""
1430 return not flight.entranceExam and \
1431 flight.stage==const.STAGE_PUSHANDTAXI and \
1432 PayloadChecker.isZFWFaulty(state.zfw, flight.zfw)
1433
1434 def logFault(self, flight, aircraft, logger, oldState, state):
1435 """Log the fault."""
1436 flight.handleNoGo(PayloadChecker, state.timestamp,
1437 "ZFW difference is more than %d kgs" % \
1438 (PayloadChecker.TOLERANCE,),
1439 "ZFW NO GO")
1440
1441#---------------------------------------------------------------------------------------
1442
1443class PitotChecker(PatientFaultChecker):
1444 """Check if pitot heat is on."""
1445 def __init__(self):
1446 """Construct the checker."""
1447 super(PitotChecker, self).__init__(timeout = 3.0)
1448
1449 def isCondition(self, flight, aircraft, oldState, state):
1450 """Check if the fault condition holds."""
1451 return state.groundSpeed>80 and state.pitotHeatOn is False
1452
1453 def logFault(self, flight, aircraft, logger, oldState, state):
1454 """Log the fault."""
1455 score = 2 if flight.stage in [const.STAGE_TAKEOFF, const.STAGE_CLIMB,
1456 const.STAGE_CRUISE, const.STAGE_DESCENT,
1457 const.STAGE_LANDING] else 0
1458 flight.handleFault(PitotChecker, state.timestamp,
1459 FaultChecker._appendDuring(flight, "Pitot heat was off"),
1460 score)
1461
1462#---------------------------------------------------------------------------------------
1463
1464class ReverserLogger(StateChecker):
1465 """Logger for the reverser."""
1466 def check(self, flight, aircraft, logger, oldState, state):
1467 """Log the cruise speed if necessary."""
1468 if oldState is not None and state.reverser != oldState.reverser:
1469 reverser = max(state.reverser)
1470 logger.message(state.timestamp,
1471 "Reverser " + ("unlocked" if reverser else "closed") +
1472 (" - %.0f %s" % (flight.speedFromKnots(state.ias),
1473 flight.getEnglishSpeedUnit())))
1474
1475#---------------------------------------------------------------------------------------
1476
1477class ReverserChecker(SimpleFaultChecker):
1478 """Check if the reverser is not used below the speed prescribed for the
1479 aircraft."""
1480 def isCondition(self, flight, aircraft, oldState, state):
1481 """Check if the fault condition holds."""
1482 return flight.stage in [const.STAGE_DESCENT, const.STAGE_LANDING,
1483 const.STAGE_TAXIAFTERLAND] and \
1484 state.reverser and \
1485 state.ias<aircraft.reverseMinSpeed and max(state.reverser)
1486
1487 def logFault(self, flight, aircraft, logger, oldState, state):
1488 """Log the fault."""
1489 message = "Reverser used below %.0f %s" % \
1490 (flight.speedFromKnots(aircraft.reverseMinSpeed), flight.getEnglishSpeedUnit())
1491 flight.handleFault(ReverserChecker, state.timestamp,
1492 FaultChecker._appendDuring(flight, message),
1493 15)
1494
1495#---------------------------------------------------------------------------------------
1496
1497class SpeedChecker(StateChecker):
1498 """Checker for the ground speed of aircraft.
1499
1500 If, during the PUSHANDTAXI stage the speed exceeds 50 knots, the state is
1501 saved as a provisional takeoff state. If the speed then decreases below 50
1502 knots, or the plane remains on the ground for more than 40 seconds, a taxi
1503 speed error is logged with the highest ground speed detected. This state is
1504 also stored in the flight object as a possible
1505
1506 If the plane becomes airborne within 40 seconds, the stage becomes TAKEOFF,
1507 and the previously saved takeoff state is logged.
1508
1509 During the TAXIAFTERLAND stage, if the speed goes above 50 knots, a fault
1510 is logged based on the actual speed."""
1511 @staticmethod
1512 def logSpeedFault(flight, state, stage = None, updateID = None):
1513 """Log the speed fault."""
1514 message = "Taxi speed over %.0f %s" % \
1515 (flight.speedFromKnots(50), flight.getEnglishSpeedUnit())
1516 if stage is None:
1517 stage = flight.stage
1518 score = FaultChecker._getLinearScore(50, 80, 10, 15, state.groundSpeed)
1519 return flight.handleFault((SpeedChecker, stage), state.timestamp,
1520 FaultChecker._appendDuring(flight, message),
1521 score, updatePrevious = updateID is None,
1522 updateID = updateID)
1523
1524 def __init__(self):
1525 """Initialize the speed checker."""
1526 self._takeoffState = None
1527 self._highestSpeedState = None
1528
1529 def check(self, flight, aircraft, logger, oldState, state):
1530 """Check the state as described above."""
1531 if flight.stage==const.STAGE_PUSHANDTAXI or \
1532 flight.stage==const.STAGE_RTO:
1533 if state.groundSpeed>50 and aircraft.hasStrobeLight and \
1534 state.strobeLightsOn is False:
1535 self.logSpeedFault(flight, state)
1536 else:
1537 self._checkPushAndTaxi(flight, aircraft, state)
1538 elif flight.stage==const.STAGE_TAXIAFTERLAND:
1539 if state.groundSpeed>50:
1540 self.logSpeedFault(flight, state)
1541 else:
1542 self._takeoffState = None
1543
1544 def _checkPushAndTaxi(self, flight, aircraft, state):
1545 """Check the speed during the push and taxi stage."""
1546 if state.groundSpeed>50:
1547 if self._takeoffState is None:
1548 self._takeoffState = state
1549 self._highestSpeedState = state
1550 else:
1551 if state.groundSpeed>self._highestSpeedState.groundSpeed:
1552 self._highestSpeedState = state
1553
1554 if not state.onTheGround:
1555 aircraft.setStage(self._takeoffState, const.STAGE_TAKEOFF)
1556 self._takeoffState = None
1557 elif state.timestamp > (self._takeoffState.timestamp + 60):
1558 flight.setRTOState(self._highestSpeedState)
1559 elif self._takeoffState is not None:
1560 flight.setRTOState(self._highestSpeedState)
1561 self._takeoffState = None
1562
1563#---------------------------------------------------------------------------------------
1564
1565class StrobeLightsChecker(PatientFaultChecker):
1566 """Check if the strobe lights are used properly."""
1567 def isCondition(self, flight, aircraft, oldState, state):
1568 """Check if the fault condition holds."""
1569 return state.strobeLightsOn is not None and \
1570 ((flight.stage==const.STAGE_BOARDING and \
1571 state.strobeLightsOn and state.onTheGround) or \
1572 (flight.stage==const.STAGE_TAKEOFF and \
1573 not state.strobeLightsOn and not state.gearsDown) or \
1574 (flight.stage in [const.STAGE_CLIMB, const.STAGE_CRUISE,
1575 const.STAGE_DESCENT] and \
1576 not state.strobeLightsOn and not state.onTheGround) or \
1577 (flight.stage==const.STAGE_PARKING and \
1578 state.strobeLightsOn and state.onTheGround))
1579
1580 def logFault(self, flight, aircraft, logger, oldState, state):
1581 """Log the fault."""
1582 message = "Strobe lights were %s" % (("on" if state.strobeLightsOn else "off"),)
1583 flight.handleFault(StrobeLightsChecker, state.timestamp,
1584 FaultChecker._appendDuring(flight, message),
1585 1)
1586
1587#---------------------------------------------------------------------------------------
1588
1589class ThrustChecker(SimpleFaultChecker):
1590 """Check if the thrust setting is not too high during takeoff.
1591
1592 FIXME: is this really so general, for all aircraft?"""
1593 def isCondition(self, flight, aircraft, oldState, state):
1594 """Check if the fault condition holds."""
1595 if flight.stage==const.STAGE_TAKEOFF and state.n1 is not None:
1596 for n1 in state.n1:
1597 if n1 is not None and n1>97:
1598 return True
1599 return False
1600
1601 def logFault(self, flight, aircraft, logger, oldState, state):
1602 """Log the fault."""
1603 flight.handleFault(ThrustChecker, state.timestamp,
1604 FaultChecker._appendDuring(flight,
1605 "Thrust setting was too high (>97%)"),
1606 FaultChecker._getLinearScore(97, 110, 0, 10,
1607 max(state.n1)),
1608 updatePrevious = True)
1609
1610#---------------------------------------------------------------------------------------
1611
1612class VSChecker(SimpleFaultChecker):
1613 """Check if the vertical speed is not too low at certain altitudes"""
1614 BELOW10000 = -5000
1615 BELOW5000 = -2500
1616 BELOW2500 = -1500
1617 BELOW500 = -1000
1618 TOLERANCE = 1.2
1619
1620 def isCondition(self, flight, aircraft, oldState, state):
1621 """Check if the fault condition holds."""
1622 vs = state.smoothedVS
1623 altitude = state.altitude
1624 return vs < -8000 or vs > 8000 or \
1625 (altitude<500 and vs < (VSChecker.BELOW500 *
1626 VSChecker.TOLERANCE)) or \
1627 (altitude<2500 and vs < (VSChecker.BELOW2500 *
1628 VSChecker.TOLERANCE)) or \
1629 (altitude<5000 and vs < (VSChecker.BELOW5000 *
1630 VSChecker.TOLERANCE)) or \
1631 (altitude<10000 and vs < (VSChecker.BELOW10000 *
1632 VSChecker.TOLERANCE))
1633
1634 def logFault(self, flight, aircraft, logger, oldState, state):
1635 """Log the fault."""
1636 vs = state.smoothedVS
1637
1638 message = "Vertical speed was %.0f feet/min" % (vs,)
1639 if vs>-8000 and vs<8000:
1640 message += " at %.0f feet (exceeds company limit)" % (state.altitude,)
1641
1642 score = 10 if vs<-8000 or vs>8000 else 0
1643
1644 flight.handleFault(VSChecker, state.timestamp,
1645 FaultChecker._appendDuring(flight, message),
1646 score)
1647
1648#---------------------------------------------------------------------------------------
Note: See TracBrowser for help on using the repository browser.