source: src/mlx/checks.py@ 1179:7cbf103d3785

python3
Last change on this file since 1179:7cbf103d3785 was 1165:3f5bc889907b, checked in by István Váradi <ivaradi@…>, 5 months ago

The altitude above which the speed is reported in Mach can be specified for each aircraft type (re #389).

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