source: src/mlx/checks.py@ 633:940d5bc945a7

Last change on this file since 633:940d5bc945a7 was 631:5801fb8d2d03, checked in by István Váradi <ivaradi@…>, 10 years ago

Added the landing lights checker for Tupolevs (re #263)

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