source: src/mlx/checks.py

python3
Last change on this file was 1128:3a549693a614, checked in by István Váradi <ivaradi@…>, 2 months ago

The grace period for the anti-collision lights is increased to 5 seconds (re #382)

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