source: src/mlx/checks.py@ 322:1189006d3996

Last change on this file since 322:1189006d3996 was 321:8f6b55fb98ed, checked in by István Váradi <ivaradi@…>, 12 years ago

Added the forced logging of radio frequencies when entering certain stages of the flight

File size: 52.3 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_PUSHANDTAXI or stage==const.STAGE_RTO:
58 if state.landingLightsOn or state.strobeLightsOn or \
59 state.groundSpeed>80.0:
60 aircraft.setStage(state, const.STAGE_TAKEOFF)
61 elif stage==const.STAGE_TAKEOFF:
62 if not state.gearsDown or \
63 (state.radioAltitude>3000.0 and state.vs>0):
64 aircraft.setStage(state, const.STAGE_CLIMB)
65 elif not state.landingLightsOn and \
66 not state.strobeLightsOn and \
67 state.onTheGround and \
68 state.groundSpeed<50.0:
69 aircraft.setStage(state, const.STAGE_RTO)
70 elif stage==const.STAGE_CLIMB:
71 if (state.altitude+2000) > flight.cruiseAltitude:
72 aircraft.setStage(state, const.STAGE_CRUISE)
73 elif state.radioAltitude<2000.0 and \
74 state.vs < 0.0 and state.gearsDown:
75 aircraft.setStage(state, const.STAGE_LANDING)
76 elif stage==const.STAGE_CRUISE:
77 if (state.altitude+2000) < flight.cruiseAltitude:
78 aircraft.setStage(state, const.STAGE_DESCENT)
79 elif stage==const.STAGE_DESCENT or stage==const.STAGE_GOAROUND:
80 if state.gearsDown and state.radioAltitude<2000.0:
81 aircraft.setStage(state, const.STAGE_LANDING)
82 elif (state.altitude+2000) > flight.cruiseAltitude:
83 aircraft.setStage(state, const.STAGE_CRUISE)
84 elif stage==const.STAGE_LANDING:
85 if state.onTheGround and state.groundSpeed<50.0:
86 aircraft.setStage(state, const.STAGE_TAXIAFTERLAND)
87 elif not state.gearsDown:
88 aircraft.setStage(state, const.STAGE_GOAROUND)
89 elif state.radioAltitude>200 and self._flareStarted:
90 aircraft.cancelFlare()
91 self._flareStarted = False
92 elif state.radioAltitude<150 and not state.onTheGround and \
93 not self._flareStarted:
94 self._flareStarted = True
95 aircraft.prepareFlare()
96 elif stage==const.STAGE_TAXIAFTERLAND:
97 if state.parking:
98 aircraft.setStage(state, const.STAGE_PARKING)
99 elif stage==const.STAGE_PARKING:
100 if aircraft.checkFlightEnd(state):
101 aircraft.setStage(state, const.STAGE_END)
102
103#---------------------------------------------------------------------------------------
104
105class ACARSSender(StateChecker):
106 """Sender of online ACARS.
107
108 It sends the ACARS every 3 minutes to the MAVA website."""
109
110 ## The interval at which the ACARS are sent
111 INTERVAL = 3*60.0
112
113 def __init__(self, gui):
114 """Construct the ACARS sender."""
115 self._gui = gui
116 self._lastSent = None
117
118 def check(self, flight, aircraft, logger, oldState, state):
119 """If the time has come to send the ACARS, send it."""
120 now = time.time()
121
122 if self._lastSent is not None and \
123 (self._lastSent + ACARSSender.INTERVAL)>now:
124 return
125
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
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 "Centre of gravity: %.1f%%" % (state.cog*100.0,))
160 self._onTheGround = False
161
162#---------------------------------------------------------------------------------------
163
164class CruiseSpeedLogger(StateChecker):
165 """Logger for the cruise speed."""
166 def __init__(self):
167 """Construct the logger."""
168 self._lastTime = None
169
170 def check(self, flight, aircraft, logger, oldState, state):
171 """Log the cruise speed if necessary."""
172 if flight.stage==const.STAGE_CRUISE and \
173 (self._lastTime is None or \
174 (self._lastTime+800)<=state.timestamp):
175 if state.altitude>24500.0:
176 logger.message(state.timestamp,
177 "Cruise speed: %.3f mach" % (state.mach,))
178 else:
179 logger.message(state.timestamp,
180 "Cruise speed: %.0f %s" %
181 (flight.speedFromKnots(state.ias),
182 flight.getEnglishSpeedUnit()))
183 self._lastTime = state.timestamp
184
185#---------------------------------------------------------------------------------------
186
187class SpoilerLogger(StateChecker):
188 """Logger for the cruise speed."""
189 def __init__(self):
190 """Construct the logger."""
191 self._logged = False
192 self._spoilersExtension = None
193
194 def check(self, flight, aircraft, logger, oldState, state):
195 """Log the cruise speed if necessary."""
196 if flight.stage==const.STAGE_LANDING and not self._logged:
197 if state.onTheGround:
198 if state.spoilersExtension!=self._spoilersExtension:
199 logger.message(state.timestamp, "Spoilers deployed")
200 self._logged = True
201 config = flight.config
202 if config.enableSounds and config.speedbrakeAtTD:
203 startSound(const.SOUND_SPEEDBRAKE)
204 else:
205 self._spoilersExtension = state.spoilersExtension
206
207#---------------------------------------------------------------------------------------
208
209class VisibilityChecker(StateChecker):
210 """Inform the pilot of the visibility once when descending below 2000 ft,
211 then when descending below 1000 ft."""
212 def __init__(self):
213 """Construct the visibility checker."""
214 self._informedBelow2000 = False
215 self._informedBelow1000 = False
216
217 def check(self, flight, aircraft, logger, oldState, state):
218 """Check if we need to inform the pilot of the visibility."""
219 if flight.stage==const.STAGE_DESCENT or \
220 flight.stage==const.STAGE_LANDING:
221 if (state.radioAltitude<2000 and not self._informedBelow2000) or \
222 (state.radioAltitude<1000 and not self._informedBelow1000):
223 visibilityString = util.visibility2String(state.visibility)
224 fs.sendMessage(const.MESSAGETYPE_VISIBILITY,
225 "Current visibility: " + visibilityString,
226 5)
227 logger.message(state.timestamp,
228 "Pilot was informed about the visibility: " +
229 visibilityString)
230 self._informedBelow2000 = True
231 self._informedBelow1000 = state.radioAltitude<1000
232
233#---------------------------------------------------------------------------------------
234
235class ApproachCalloutsPlayer(StateChecker):
236 """A state checker that plays a sequence of approach callouts.
237
238 It tracks the altitude during the descent and landing phases and
239 if the altitude crosses one that has a callout associated with and
240 the vertical speed is negative, that callout will be played."""
241 def __init__(self, approachCallouts):
242 """Construct the approach callouts player."""
243 self._approachCallouts = approachCallouts
244 self._altitudes = approachCallouts.getAltitudes(descending = False)
245
246 def check(self, flight, aircraft, logger, oldState, state):
247 """Check if we need to play a callout."""
248 if (flight.stage==const.STAGE_DESCENT or \
249 flight.stage==const.STAGE_LANDING) and state.vs<0:
250 oldRadioAltitude = oldState.radioAltitude
251 radioAltitude = state.radioAltitude
252 for altitude in self._altitudes:
253 if radioAltitude<=altitude and \
254 oldRadioAltitude>altitude:
255 startSound(self._approachCallouts[altitude])
256 break
257
258#---------------------------------------------------------------------------------------
259
260class StateChangeLogger(StateChecker):
261 """Base class for classes the instances of which check if a specific change has
262 occured in the aircraft's state, and log such change."""
263 def __init__(self, logInitial = True):
264 """Construct the logger.
265
266 If logInitial is True, the initial value will be logged, not just the
267 changes later.
268
269 Child classes should define the following functions:
270 - _changed(self, oldState, state): returns a boolean indicating if the
271 value has changed or not
272 - _getMessage(self, flight, state, forced): return a strings containing
273 the message to log with the new value
274 """
275 self._logInitial = logInitial
276
277 def _getLogTimestamp(self, state):
278 """Get the log timestamp."""
279 return state.timestamp
280
281 def check(self, flight, aircraft, logger, oldState, state):
282 """Check if the state has changed, and if so, log the new state."""
283 shouldLog = False
284 if oldState is None:
285 shouldLog = self._logInitial
286 else:
287 shouldLog = self._changed(oldState, state)
288
289 if shouldLog:
290 self.logState(flight, logger, state)
291
292 def logState(self, flight, logger, state, forced = False):
293 """Log the state."""
294 message = self._getMessage(flight, state, forced)
295 if message is not None:
296 logger.message(self._getLogTimestamp(state), message)
297
298#-------------------------------------------------------------------------------
299
300class SimpleChangeMixin(object):
301 """A mixin that defines a _changed() function which simply calls a function
302 to retrieve the value two monitor for both states and compares them.
303
304 Child classes should define the following function:
305 - _getValue(state): get the value we are interested in."""
306 def _changed(self, oldState, state):
307 """Determine if the value has changed."""
308 currentValue = self._getValue(state)
309 return currentValue is not None and self._getValue(oldState)!=currentValue
310
311#---------------------------------------------------------------------------------------
312
313class SingleValueMixin(object):
314 """A mixin that provides a _getValue() function to query a value from the
315 state using the name of the attribute."""
316 def __init__(self, attrName):
317 """Construct the mixin with the given attribute name."""
318 self._attrName = attrName
319
320 def _getValue(self, state):
321 """Get the value of the attribute from the state."""
322 return getattr(state, self._attrName)
323
324#---------------------------------------------------------------------------------------
325
326class DelayedChangeMixin(object):
327 """A mixin to a StateChangeLogger that stores the old value and reports a
328 change only if the change has been there for a certain amount of time.
329
330 Child classes should define the following function:
331 - _getValue(state): get the value we are interested in."""
332 def __init__(self, minDelay = 3.0, maxDelay = 10.0):
333 """Construct the mixin with the given delay in seconds."""
334 self._minDelay = minDelay
335 self._maxDelay = maxDelay
336 self._oldValue = None
337 self._firstChange = None
338 self._lastChangeState = None
339
340 def _changed(self, oldState, state):
341 """Determine if the value has changed."""
342 if self._oldValue is None:
343 self._oldValue = self._getValue(oldState)
344
345 newValue = self._getValue(state)
346 if self._isDifferent(self._oldValue, newValue):
347 if self._firstChange is None:
348 self._firstChange = state.timestamp
349 self._lastChangeState = state
350 self._oldValue = newValue
351
352 if self._firstChange is not None:
353 if state.timestamp >= min(self._lastChangeState.timestamp + self._minDelay,
354 self._firstChange + self._maxDelay):
355 self._firstChange = None
356 return True
357
358 return False
359
360 def _getLogTimestamp(self, state):
361 """Get the log timestamp."""
362 return self._lastChangeState.timestamp if \
363 self._lastChangeState is not None else state.timestamp
364
365 def _isDifferent(self, oldValue, newValue):
366 """Determine if the given values are different.
367
368 This default implementation checks for simple equality."""
369 return oldValue!=newValue
370
371#---------------------------------------------------------------------------------------
372
373class TemplateMessageMixin(object):
374 """Mixin to generate a message based on a template.
375
376 Child classes should define the following function:
377 - _getValue(state): get the value we are interested in."""
378 def __init__(self, template):
379 """Construct the mixin."""
380 self._template = template
381
382 def _getMessage(self, flight, state, forced):
383 """Get the message."""
384 value = self._getValue(state)
385 return None if value is None else self._template % (value,)
386
387#---------------------------------------------------------------------------------------
388
389class GenericStateChangeLogger(StateChangeLogger, SingleValueMixin,
390 DelayedChangeMixin, TemplateMessageMixin):
391 """Base for generic state change loggers that monitor a single value in the
392 state possibly with a delay and the logged message comes from a template"""
393 def __init__(self, attrName, template, logInitial = True,
394 minDelay = 0.0, maxDelay = 0.0):
395 """Construct the object."""
396 StateChangeLogger.__init__(self, logInitial = logInitial)
397 SingleValueMixin.__init__(self, attrName)
398 DelayedChangeMixin.__init__(self, minDelay = minDelay, maxDelay = maxDelay)
399 TemplateMessageMixin.__init__(self, template)
400 self._getLogTimestamp = lambda state: \
401 DelayedChangeMixin._getLogTimestamp(self, state)
402
403#---------------------------------------------------------------------------------------
404
405class ForceableLoggerMixin(object):
406 """A mixin for loggers that can be forced to log a certain state.
407
408 The last logged state is always maintained, and when checking for a change,
409 that state is compared to the current one (which may actually be the same,
410 if a forced logging was performed for that state).
411
412 Children should implement the following functions:
413 - _hasChanged(oldState, state): the real check for a change
414 - _logState(flight, logger, state, forced): the real logging function
415 """
416 def __init__(self):
417 """Construct the mixin."""
418 self._lastLoggedState = None
419
420 def forceLog(self, flight, logger, state):
421 """Force logging the given state."""
422 self.logState(flight, logger, state, forced = True)
423
424 def logState(self, flight, logger, state, forced = False):
425 """Log the state.
426
427 It calls _logState to perform the real logging, and saves the given
428 state as the last logged one."""
429 self._logState(flight, logger, state, forced)
430 self._lastLoggedState = state
431
432 def _changed(self, oldState, state):
433 """Check if the state has changed.
434
435 This function calls _hasChanged for the real check, and replaces
436 oldState with the stored last logged state, if any."""
437 if self._lastLoggedState is not None:
438 oldState = self._lastLoggedState
439 return self._hasChanged(oldState, state)
440
441#---------------------------------------------------------------------------------------
442
443class AltimeterLogger(StateChangeLogger, SingleValueMixin,
444 DelayedChangeMixin):
445 """Logger for the altimeter setting."""
446 def __init__(self):
447 """Construct the logger."""
448 StateChangeLogger.__init__(self, logInitial = True)
449 SingleValueMixin.__init__(self, "altimeter")
450 DelayedChangeMixin.__init__(self)
451 self._getLogTimestamp = lambda state: \
452 DelayedChangeMixin._getLogTimestamp(self, state)
453
454 def _getMessage(self, flight, state, forced):
455 """Get the message to log on a change."""
456 logState = self._lastChangeState if \
457 self._lastChangeState is not None else state
458 return "Altimeter: %.0f hPa at %.0f feet" % \
459 (logState.altimeter, logState.altitude)
460
461#---------------------------------------------------------------------------------------
462
463class NAVLogger(StateChangeLogger, DelayedChangeMixin, ForceableLoggerMixin):
464 """Logger for NAV radios.
465
466 It also logs the OBS frequency set."""
467 @staticmethod
468 def getMessage(logName, frequency, obs):
469 """Get the message for the given NAV radio setting."""
470 message = u"%s frequency: %s MHz" % (logName, frequency)
471 if obs is not None: message += u" [%d\u00b0]" % (obs,)
472 return message
473
474 def __init__(self, attrName, logName):
475 """Construct the NAV logger."""
476 StateChangeLogger.__init__(self, logInitial = True)
477 DelayedChangeMixin.__init__(self)
478 ForceableLoggerMixin.__init__(self)
479
480 self.logState = lambda flight, logger, state, forced = False: \
481 ForceableLoggerMixin.logState(self, flight, logger, state,
482 forced = forced)
483 self._getLogTimestamp = \
484 lambda state: DelayedChangeMixin._getLogTimestamp(self, state)
485 self._changed = lambda oldState, state: \
486 ForceableLoggerMixin._changed(self, oldState, state)
487 self._hasChanged = lambda oldState, state: \
488 DelayedChangeMixin._changed(self, oldState, state)
489 self._logState = lambda flight, logger, state, forced: \
490 StateChangeLogger.logState(self, flight, logger, state,
491 forced = forced)
492
493 self._attrName = attrName
494 self._logName = logName
495
496 def _getValue(self, state):
497 """Get the value.
498
499 If both the frequency and the obs settings are available, a tuple
500 containing them is returned, otherwise None."""
501 frequency = getattr(state, self._attrName)
502 obs = getattr(state, self._attrName + "_obs")
503 manual = getattr(state, self._attrName + "_manual")
504 return (frequency, obs, manual)
505
506 def _getMessage(self, flight, state, forced):
507 """Get the message."""
508 (frequency, obs, manual) = self._getValue(state)
509 return None if frequency is None or obs is None or \
510 (not manual and not forced) else \
511 self.getMessage(self._logName, frequency, obs)
512
513 def _isDifferent(self, oldValue, newValue):
514 """Determine if the valie has changed between the given states."""
515 (oldFrequency, oldOBS, _oldManual) = oldValue
516 (newFrequency, newOBS, _newManual) = newValue
517 return oldFrequency!=newFrequency or oldOBS!=newOBS
518
519#---------------------------------------------------------------------------------------
520
521class NAV1Logger(NAVLogger):
522 """Logger for the NAV1 radio setting."""
523 def __init__(self):
524 """Construct the logger."""
525 super(NAV1Logger, self).__init__("nav1", "NAV1")
526
527#---------------------------------------------------------------------------------------
528
529class NAV2Logger(NAVLogger):
530 """Logger for the NAV2 radio setting."""
531 def __init__(self):
532 """Construct the logger."""
533 super(NAV2Logger, self).__init__("nav2", "NAV2")
534
535#---------------------------------------------------------------------------------------
536
537class ADFLogger(GenericStateChangeLogger, ForceableLoggerMixin):
538 """Base class for the ADF loggers."""
539 def __init__(self, attr, logName):
540 """Construct the ADF logger."""
541 GenericStateChangeLogger.__init__(self, attr,
542 "%s frequency: %%s kHz" % (logName,),
543 minDelay = 3.0, maxDelay = 10.0)
544 ForceableLoggerMixin.__init__(self)
545
546 self.logState = lambda flight, logger, state, forced = False: \
547 ForceableLoggerMixin.logState(self, flight, logger, state,
548 forced = forced)
549 self._changed = lambda oldState, state: \
550 ForceableLoggerMixin._changed(self, oldState, state)
551 self._hasChanged = lambda oldState, state: \
552 DelayedChangeMixin._changed(self, oldState, state)
553 self._logState = lambda flight, logger, state, forced: \
554 StateChangeLogger.logState(self, flight, logger, state, forced)
555
556#---------------------------------------------------------------------------------------
557
558class ADF1Logger(ADFLogger):
559 """Logger for the ADF1 radio setting."""
560 def __init__(self):
561 """Construct the logger."""
562 super(ADF1Logger, self).__init__("adf1", "ADF1")
563
564#---------------------------------------------------------------------------------------
565
566class ADF2Logger(ADFLogger):
567 """Logger for the ADF2 radio setting."""
568 def __init__(self):
569 """Construct the logger."""
570 super(ADF2Logger, self).__init__("adf2", "ADF2")
571
572#---------------------------------------------------------------------------------------
573
574class SquawkLogger(GenericStateChangeLogger):
575 """Logger for the squawk setting."""
576 def __init__(self):
577 """Construct the logger."""
578 super(SquawkLogger, self).__init__("squawk", "Squawk code: %s",
579 minDelay = 3.0, maxDelay = 10.0)
580
581#---------------------------------------------------------------------------------------
582
583class LightsLogger(StateChangeLogger, SingleValueMixin, SimpleChangeMixin):
584 """Base class for the loggers of the various lights."""
585 def __init__(self, attrName, template):
586 """Construct the logger."""
587 StateChangeLogger.__init__(self)
588 SingleValueMixin.__init__(self, attrName)
589
590 self._template = template
591
592 def _getMessage(self, flight, state, forced):
593 """Get the message from the given state."""
594 return self._template % ("ON" if self._getValue(state) else "OFF")
595
596#---------------------------------------------------------------------------------------
597
598class AnticollisionLightsLogger(LightsLogger):
599 """Logger for the anti-collision lights."""
600 def __init__(self):
601 LightsLogger.__init__(self, "antiCollisionLightsOn",
602 "Anti-collision lights: %s")
603
604#---------------------------------------------------------------------------------------
605
606class LandingLightsLogger(LightsLogger):
607 """Logger for the landing lights."""
608 def __init__(self):
609 LightsLogger.__init__(self, "landingLightsOn",
610 "Landing lights: %s")
611
612#---------------------------------------------------------------------------------------
613
614class StrobeLightsLogger(LightsLogger):
615 """Logger for the strobe lights."""
616 def __init__(self):
617 LightsLogger.__init__(self, "strobeLightsOn",
618 "Strobe lights: %s")
619
620#---------------------------------------------------------------------------------------
621
622class NavLightsLogger(LightsLogger):
623 """Logger for the navigational lights."""
624 def __init__(self):
625 LightsLogger.__init__(self, "navLightsOn",
626 "Navigational lights: %s")
627
628#---------------------------------------------------------------------------------------
629
630class FlapsLogger(StateChangeLogger, SingleValueMixin, SimpleChangeMixin):
631 """Logger for the flaps setting."""
632 def __init__(self):
633 """Construct the logger."""
634 StateChangeLogger.__init__(self, logInitial = True)
635 SingleValueMixin.__init__(self, "flapsSet")
636
637 def _getMessage(self, flight, state, forced):
638 """Get the message to log on a change."""
639 speed = state.groundSpeed if state.groundSpeed<80.0 else state.ias
640 return "Flaps set to %.0f at %.0f %s" % \
641 (state.flapsSet, flight.speedFromKnots(speed),
642 flight.getEnglishSpeedUnit())
643
644#---------------------------------------------------------------------------------------
645
646class GearsLogger(StateChangeLogger, SingleValueMixin, SimpleChangeMixin):
647 """Logger for the gears state."""
648 def __init__(self):
649 """Construct the logger."""
650 StateChangeLogger.__init__(self, logInitial = True)
651 SingleValueMixin.__init__(self, "gearControlDown")
652
653 def _getMessage(self, flight, state, forced):
654 """Get the message to log on a change."""
655 return "Gears SET to %s at %.0f %s, %.0f feet" % \
656 ("DOWN" if state.gearControlDown else "UP",
657 flight.speedFromKnots(state.ias),
658 flight.getEnglishSpeedUnit(), state.altitude)
659
660#---------------------------------------------------------------------------------------
661
662class FaultChecker(StateChecker):
663 """Base class for checkers that look for faults."""
664 @staticmethod
665 def _appendDuring(flight, message):
666 """Append a 'during XXX' test to the given message, depending on the
667 flight stage."""
668 stageStr = const.stage2string(flight.stage)
669 return message if stageStr is None \
670 else (message + " during " + stageStr.upper())
671
672 @staticmethod
673 def _getLinearScore(minFaultValue, maxFaultValue, minScore, maxScore,
674 value):
675 """Get the score for a faulty value where the score is calculated
676 linearly within a certain range."""
677 if value<minFaultValue:
678 return 0
679 elif value>maxFaultValue:
680 return maxScore
681 else:
682 return minScore + (maxScore-minScore) * (value-minFaultValue) / \
683 (maxFaultValue - minFaultValue)
684
685#---------------------------------------------------------------------------------------
686
687class SimpleFaultChecker(FaultChecker):
688 """Base class for fault checkers that check for a single occurence of a
689 faulty condition.
690
691 Child classes should implement the following functions:
692 - isCondition(self, flight, aircraft, oldState, state): should return whether the
693 condition holds
694 - logFault(self, flight, aircraft, logger, oldState, state): log the fault
695 via the logger."""
696 def check(self, flight, aircraft, logger, oldState, state):
697 """Perform the check."""
698 if self.isCondition(flight, aircraft, oldState, state):
699 self.logFault(flight, aircraft, logger, oldState, state)
700
701#---------------------------------------------------------------------------------------
702
703class PatientFaultChecker(FaultChecker):
704 """A fault checker that does not decides on a fault when the condition
705 arises immediately, but can wait some time.
706
707 Child classes should implement the following functions:
708 - isCondition(self, flight, aircraft, oldState, state): should return whether the
709 condition holds
710 - logFault(self, flight, aircraft, logger, oldState, state): log the fault
711 via the logger
712 """
713 def __init__(self, timeout = 2.0):
714 """Construct the fault checker with the given timeout."""
715 self._timeout = timeout
716 self._faultStarted = None
717
718 def getTimeout(self, flight, aircraft, oldState, state):
719 """Get the timeout.
720
721 This default implementation returns the timeout given in the
722 constructor, but child classes might want to enforce a different
723 policy."""
724 return self._timeout
725
726 def check(self, flight, aircraft, logger, oldState, state):
727 """Perform the check."""
728 if self.isCondition(flight, aircraft, oldState, state):
729 if self._faultStarted is None:
730 self._faultStarted = state.timestamp
731 timeout = self.getTimeout(flight, aircraft, oldState, state)
732 if state.timestamp>=(self._faultStarted + timeout):
733 self.logFault(flight, aircraft, logger, oldState, state)
734 self._faultStarted = state.timestamp
735 else:
736 self._faultStarted = None
737
738#---------------------------------------------------------------------------------------
739
740class AntiCollisionLightsChecker(PatientFaultChecker):
741 """Check for the anti-collision light being off at high N1 values."""
742 def isCondition(self, flight, aircraft, oldState, state):
743 """Check if the fault condition holds."""
744 return (flight.stage!=const.STAGE_PARKING or \
745 not flight.config.usingFS2Crew) and \
746 not state.antiCollisionLightsOn and \
747 self.isEngineCondition(state)
748
749 def logFault(self, flight, aircraft, logger, oldState, state):
750 """Log the fault."""
751 flight.handleFault(AntiCollisionLightsChecker, state.timestamp,
752 FaultChecker._appendDuring(flight,
753 "Anti-collision lights were off"),
754 1)
755
756 def isEngineCondition(self, state):
757 """Determine if the engines are in such a state that the lights should
758 be on."""
759 if state.n1 is not None:
760 return max(state.n1)>5
761 elif state.rpm is not None:
762 return max(state.rpm)>0
763 else:
764 return False
765
766#---------------------------------------------------------------------------------------
767
768class TupolevAntiCollisionLightsChecker(AntiCollisionLightsChecker):
769 """Check for the anti-collision light for Tuplev planes."""
770 def isCondition(self, flight, aircraft, oldState, state):
771 """Check if the fault condition holds."""
772 numEnginesRunning = 0
773 for n1 in state.n1:
774 if n1>5: numEnginesRunning += 1
775
776 if flight.stage==const.STAGE_PARKING:
777 return numEnginesRunning<len(state.n1) \
778 and state.antiCollisionLightsOn
779 else:
780 return numEnginesRunning>1 and not state.antiCollisionLightsOn or \
781 numEnginesRunning<1 and state.antiCollisionLightsOn
782
783#---------------------------------------------------------------------------------------
784
785class BankChecker(SimpleFaultChecker):
786 """Check for the bank is within limits."""
787 def isCondition(self, flight, aircraft, oldState, state):
788 """Check if the fault condition holds."""
789 if flight.stage==const.STAGE_CRUISE:
790 bankLimit = 30
791 elif flight.stage in [const.STAGE_TAKEOFF, const.STAGE_CLIMB,
792 const.STAGE_DESCENT, const.STAGE_LANDING]:
793 bankLimit = 35
794 else:
795 return False
796
797 return state.bank>bankLimit or state.bank<-bankLimit
798
799 def logFault(self, flight, aircraft, logger, oldState, state):
800 """Log the fault."""
801 flight.handleFault(BankChecker, state.timestamp,
802 FaultChecker._appendDuring(flight, "Bank too steep"),
803 2)
804
805#---------------------------------------------------------------------------------------
806
807class FlapsRetractChecker(SimpleFaultChecker):
808 """Check if the flaps are not retracted too early."""
809 def __init__(self):
810 """Construct the flaps checker."""
811 self._timeStart = None
812
813 def isCondition(self, flight, aircraft, oldState, state):
814 """Check if the fault condition holds.
815
816 FIXME: check if this really is the intention (FlapsRetractedMistake.java)"""
817 if (flight.stage==const.STAGE_TAKEOFF and not state.onTheGround) or \
818 (flight.stage==const.STAGE_LANDING and state.onTheGround):
819 if self._timeStart is None:
820 self._timeStart = state.timestamp
821
822 if state.flapsSet==0 and state.timestamp<=(self._timeStart+2.0):
823 return True
824 else:
825 self._timeStart = None
826 return False
827
828 def logFault(self, flight, aircraft, logger, oldState, state):
829 """Log the fault."""
830 flight.handleFault(FlapsRetractChecker, state.timestamp,
831 FaultChecker._appendDuring(flight, "Flaps retracted"),
832 20)
833
834#---------------------------------------------------------------------------------------
835
836class FlapsSpeedLimitChecker(SimpleFaultChecker):
837 """Check if the flaps are extended only at the right speeds."""
838 def isCondition(self, flight, aircraft, oldState, state):
839 """Check if the fault condition holds."""
840 speedLimit = aircraft.getFlapsSpeedLimit(state.flapsSet)
841 return speedLimit is not None and state.smoothedIAS>speedLimit
842
843 def logFault(self, flight, aircraft, logger, oldState, state):
844 """Log the fault."""
845 flight.handleFault(FlapsSpeedLimitChecker, state.timestamp,
846 FaultChecker._appendDuring(flight, "Flap speed limit fault"),
847 5)
848
849#---------------------------------------------------------------------------------------
850
851class GearsDownChecker(SimpleFaultChecker):
852 """Check if the gears are down at low altitudes."""
853 def isCondition(self, flight, aircraft, oldState, state):
854 """Check if the fault condition holds."""
855 return state.radioAltitude<10 and not state.gearsDown and \
856 flight.stage!=const.STAGE_TAKEOFF
857
858 def logFault(self, flight, aircraft, logger, oldState, state):
859 """Log the fault."""
860 flight.handleNoGo(GearsDownChecker, state.timestamp,
861 "Gears not down at %.0f feet radio altitude" % \
862 (state.radioAltitude,),
863 "GEAR DOWN NO GO")
864
865#---------------------------------------------------------------------------------------
866
867class GearSpeedLimitChecker(PatientFaultChecker):
868 """Check if the gears not down at too high a speed."""
869 def isCondition(self, flight, aircraft, oldState, state):
870 """Check if the fault condition holds."""
871 return state.gearsDown and state.smoothedIAS>aircraft.gearSpeedLimit
872
873 def logFault(self, flight, aircraft, logger, oldState, state):
874 """Log the fault."""
875 flight.handleFault(GearSpeedLimitChecker, state.timestamp,
876 FaultChecker._appendDuring(flight, "Gear speed limit fault"),
877 5)
878
879#---------------------------------------------------------------------------------------
880
881class GLoadChecker(SimpleFaultChecker):
882 """Check if the G-load does not exceed 2 except during flare."""
883 def isCondition(self, flight, aircraft, oldState, state):
884 """Check if the fault condition holds."""
885 return state.gLoad>2.0 and (flight.stage!=const.STAGE_LANDING or \
886 state.radioAltitude>=50)
887
888 def logFault(self, flight, aircraft, logger, oldState, state):
889 """Log the fault."""
890 flight.handleFault(GLoadChecker, state.timestamp,
891 "G-load was %.2f" % (state.gLoad,),
892 10)
893
894#---------------------------------------------------------------------------------------
895
896class LandingLightsChecker(PatientFaultChecker):
897 """Check if the landing lights are used properly."""
898 def getTimeout(self, flight, aircraft, oldState, state):
899 """Get the timeout.
900
901 It is the default timeout except for landing and takeoff."""
902 return 0.0 if flight.stage in [const.STAGE_TAKEOFF,
903 const.STAGE_LANDING] else self._timeout
904
905 def isCondition(self, flight, aircraft, oldState, state):
906 """Check if the fault condition holds."""
907 return state.landingLightsOn is not None and \
908 ((flight.stage==const.STAGE_BOARDING and \
909 state.landingLightsOn and state.onTheGround) or \
910 (flight.stage==const.STAGE_TAKEOFF and \
911 not state.landingLightsOn and not state.onTheGround) or \
912 (flight.stage in [const.STAGE_CLIMB, const.STAGE_CRUISE,
913 const.STAGE_DESCENT] and \
914 state.landingLightsOn and state.altitude>12500) or \
915 (flight.stage==const.STAGE_LANDING and \
916 not state.landingLightsOn and state.onTheGround) or \
917 (flight.stage==const.STAGE_PARKING and \
918 state.landingLightsOn and state.onTheGround))
919
920 def logFault(self, flight, aircraft, logger, oldState, state):
921 """Log the fault."""
922 score = 0 if flight.stage==const.STAGE_LANDING else 1
923 message = "Landing lights were %s" % (("on" if state.landingLightsOn else "off"),)
924 flight.handleFault(LandingLightsChecker, state.timestamp,
925 FaultChecker._appendDuring(flight, message),
926 score)
927
928#---------------------------------------------------------------------------------------
929
930class WeightChecker(PatientFaultChecker):
931 """Base class for checkers that check that some limit is not exceeded."""
932 def __init__(self, name):
933 """Construct the checker."""
934 super(WeightChecker, self).__init__(timeout = 5.0)
935 self._name = name
936
937 def isCondition(self, flight, aircraft, oldState, state):
938 """Check if the fault condition holds."""
939 if flight.entranceExam:
940 return False
941
942 limit = self.getLimit(flight, aircraft, state)
943 if limit is not None:
944 #if flight.options.compensation is not None:
945 # limit += flight.options.compensation
946 return self.getWeight(state)>limit
947
948 return False
949
950 def logFault(self, flight, aircraft, logger, oldState, state):
951 """Log the fault."""
952 mname = "M" + self._name
953 flight.handleNoGo(self.__class__, state.timestamp,
954 "%s exceeded: %s is %.0f kg" % \
955 (mname, self._name, self.getWeight(state)),
956 "%s NO GO" % (mname,))
957
958 def getWeight(self, state):
959 """Get the weight that is interesting for us."""
960 return state.grossWeight
961
962#---------------------------------------------------------------------------------------
963
964class MLWChecker(WeightChecker):
965 """Checks if the MLW is not exceeded on landing."""
966 def __init__(self):
967 """Construct the checker."""
968 super(MLWChecker, self).__init__("LW")
969
970 def getLimit(self, flight, aircraft, state):
971 """Get the limit if we are in the right state."""
972 return aircraft.mlw if flight.stage==const.STAGE_LANDING and \
973 state.onTheGround and \
974 not flight.entranceExam else None
975
976#---------------------------------------------------------------------------------------
977
978class MTOWChecker(WeightChecker):
979 """Checks if the MTOW is not exceeded on landing."""
980 def __init__(self):
981 """Construct the checker."""
982 super(MTOWChecker, self).__init__("TOW")
983
984 def getLimit(self, flight, aircraft, state):
985 """Get the limit if we are in the right state."""
986 return aircraft.mtow if flight.stage==const.STAGE_TAKEOFF and \
987 not flight.entranceExam else None
988
989#---------------------------------------------------------------------------------------
990
991class MZFWChecker(WeightChecker):
992 """Checks if the MZFW is not exceeded on landing."""
993 def __init__(self):
994 """Construct the checker."""
995 super(MZFWChecker, self).__init__("ZFW")
996
997 def getLimit(self, flight, aircraft, state):
998 """Get the limit if we are in the right state."""
999 return aircraft.mzfw if not flight.entranceExam else None
1000
1001 def getWeight(self, state):
1002 """Get the weight that is interesting for us."""
1003 return state.zfw
1004
1005#---------------------------------------------------------------------------------------
1006
1007class NavLightsChecker(PatientFaultChecker):
1008 """Check if the navigational lights are used properly."""
1009 def isCondition(self, flight, aircraft, oldState, state):
1010 """Check if the fault condition holds."""
1011 return flight.stage!=const.STAGE_BOARDING and \
1012 flight.stage!=const.STAGE_PARKING and \
1013 not state.navLightsOn
1014
1015 def logFault(self, flight, aircraft, logger, oldState, state):
1016 """Log the fault."""
1017 flight.handleFault(NavLightsChecker, state.timestamp,
1018 FaultChecker._appendDuring(flight,
1019 "Navigation lights were off"),
1020 1)
1021
1022#---------------------------------------------------------------------------------------
1023
1024class OverspeedChecker(PatientFaultChecker):
1025 """Check if Vne has been exceeded."""
1026 def __init__(self, timeout = 5.0):
1027 """Construct the checker."""
1028 super(OverspeedChecker, self).__init__(timeout = timeout)
1029
1030 def isCondition(self, flight, aircraft, oldState, state):
1031 """Check if the fault condition holds."""
1032 return state.overspeed
1033
1034 def logFault(self, flight, aircraft, logger, oldState, state):
1035 """Log the fault."""
1036 flight.handleFault(OverspeedChecker, state.timestamp,
1037 FaultChecker._appendDuring(flight, "Overspeed"),
1038 20)
1039
1040#---------------------------------------------------------------------------------------
1041
1042class PayloadChecker(SimpleFaultChecker):
1043 """Check if the payload matches the specification."""
1044 TOLERANCE=550
1045
1046 @staticmethod
1047 def isZFWFaulty(aircraftZFW, flightZFW):
1048 """Check if the given aircraft's ZFW is outside of the limits."""
1049 return aircraftZFW < (flightZFW - PayloadChecker.TOLERANCE) or \
1050 aircraftZFW > (flightZFW + PayloadChecker.TOLERANCE)
1051
1052 def isCondition(self, flight, aircraft, oldState, state):
1053 """Check if the fault condition holds."""
1054 return not flight.entranceExam and \
1055 flight.stage==const.STAGE_PUSHANDTAXI and \
1056 PayloadChecker.isZFWFaulty(state.zfw, flight.zfw)
1057
1058 def logFault(self, flight, aircraft, logger, oldState, state):
1059 """Log the fault."""
1060 flight.handleNoGo(PayloadChecker, state.timestamp,
1061 "ZFW difference is more than %d kgs" % \
1062 (PayloadChecker.TOLERANCE,),
1063 "ZFW NO GO")
1064
1065#---------------------------------------------------------------------------------------
1066
1067class PitotChecker(PatientFaultChecker):
1068 """Check if pitot heat is on."""
1069 def __init__(self):
1070 """Construct the checker."""
1071 super(PitotChecker, self).__init__(timeout = 3.0)
1072
1073 def isCondition(self, flight, aircraft, oldState, state):
1074 """Check if the fault condition holds."""
1075 return state.groundSpeed>80 and not state.pitotHeatOn
1076
1077 def logFault(self, flight, aircraft, logger, oldState, state):
1078 """Log the fault."""
1079 score = 2 if flight.stage in [const.STAGE_TAKEOFF, const.STAGE_CLIMB,
1080 const.STAGE_CRUISE, const.STAGE_DESCENT,
1081 const.STAGE_LANDING] else 0
1082 flight.handleFault(PitotChecker, state.timestamp,
1083 FaultChecker._appendDuring(flight, "Pitot heat was off"),
1084 score)
1085
1086#---------------------------------------------------------------------------------------
1087
1088class ReverserChecker(SimpleFaultChecker):
1089 """Check if the reverser is not used below 60 knots."""
1090 def isCondition(self, flight, aircraft, oldState, state):
1091 """Check if the fault condition holds."""
1092 return flight.stage in [const.STAGE_DESCENT, const.STAGE_LANDING,
1093 const.STAGE_TAXIAFTERLAND] and \
1094 state.groundSpeed<60 and max(state.reverser)
1095
1096 def logFault(self, flight, aircraft, logger, oldState, state):
1097 """Log the fault."""
1098 message = "Reverser used below %.0f %s" % \
1099 (flight.speedFromKnots(60), flight.getEnglishSpeedUnit())
1100 flight.handleFault(ReverserChecker, state.timestamp,
1101 FaultChecker._appendDuring(flight, message),
1102 15)
1103
1104#---------------------------------------------------------------------------------------
1105
1106class SpeedChecker(SimpleFaultChecker):
1107 """Check if the speed is in the prescribed limits."""
1108 def isCondition(self, flight, aircraft, oldState, state):
1109 """Check if the fault condition holds."""
1110 return flight.stage in [const.STAGE_PUSHANDTAXI,
1111 const.STAGE_TAXIAFTERLAND] and \
1112 state.groundSpeed>50
1113
1114 def logFault(self, flight, aircraft, logger, oldState, state):
1115 """Log the fault."""
1116 message = "Taxi speed over %.0f %s" % \
1117 (flight.speedFromKnots(50), flight.getEnglishSpeedUnit())
1118 flight.handleFault(SpeedChecker, state.timestamp,
1119 FaultChecker._appendDuring(flight, message),
1120 FaultChecker._getLinearScore(50, 80, 10, 15,
1121 state.groundSpeed))
1122
1123#---------------------------------------------------------------------------------------
1124
1125class StallChecker(PatientFaultChecker):
1126 """Check if stall occured."""
1127 def isCondition(self, flight, aircraft, oldState, state):
1128 """Check if the fault condition holds."""
1129 return flight.stage in [const.STAGE_TAKEOFF, const.STAGE_CLIMB,
1130 const.STAGE_CRUISE, const.STAGE_DESCENT,
1131 const.STAGE_LANDING] and state.stalled
1132
1133 def logFault(self, flight, aircraft, logger, oldState, state):
1134 """Log the fault."""
1135 score = 40 if flight.stage in [const.STAGE_TAKEOFF,
1136 const.STAGE_LANDING] else 30
1137 flight.handleFault(StallChecker, state.timestamp,
1138 FaultChecker._appendDuring(flight, "Stalled"),
1139 score)
1140
1141#---------------------------------------------------------------------------------------
1142
1143class StrobeLightsChecker(PatientFaultChecker):
1144 """Check if the strobe lights are used properly."""
1145 def isCondition(self, flight, aircraft, oldState, state):
1146 """Check if the fault condition holds."""
1147 return (flight.stage==const.STAGE_BOARDING and \
1148 state.strobeLightsOn and state.onTheGround) or \
1149 (flight.stage==const.STAGE_TAKEOFF and \
1150 not state.strobeLightsOn and not state.gearsDown) or \
1151 (flight.stage in [const.STAGE_CLIMB, const.STAGE_CRUISE,
1152 const.STAGE_DESCENT] and \
1153 not state.strobeLightsOn and not state.onTheGround) or \
1154 (flight.stage==const.STAGE_PARKING and \
1155 state.strobeLightsOn and state.onTheGround)
1156
1157 def logFault(self, flight, aircraft, logger, oldState, state):
1158 """Log the fault."""
1159 message = "Strobe lights were %s" % (("on" if state.strobeLightsOn else "off"),)
1160 flight.handleFault(StrobeLightsChecker, state.timestamp,
1161 FaultChecker._appendDuring(flight, message),
1162 1)
1163
1164#---------------------------------------------------------------------------------------
1165
1166class ThrustChecker(SimpleFaultChecker):
1167 """Check if the thrust setting is not too high during takeoff.
1168
1169 FIXME: is this really so general, for all aircraft?"""
1170 def isCondition(self, flight, aircraft, oldState, state):
1171 """Check if the fault condition holds."""
1172 return flight.stage==const.STAGE_TAKEOFF and \
1173 state.n1 is not None and max(state.n1)>97
1174
1175 def logFault(self, flight, aircraft, logger, oldState, state):
1176 """Log the fault."""
1177 flight.handleFault(ThrustChecker, state.timestamp,
1178 FaultChecker._appendDuring(flight,
1179 "Thrust setting was too high (>97%)"),
1180 FaultChecker._getLinearScore(97, 110, 0, 10, max(state.n1)))
1181
1182#---------------------------------------------------------------------------------------
1183
1184class VSChecker(SimpleFaultChecker):
1185 """Check if the vertical speed is not too low at certain altitudes"""
1186 BELOW10000 = -5000
1187 BELOW5000 = -2500
1188 BELOW2500 = -1500
1189 BELOW500 = -1000
1190 TOLERANCE = 1.2
1191
1192 def isCondition(self, flight, aircraft, oldState, state):
1193 """Check if the fault condition holds."""
1194 vs = state.smoothedVS
1195 altitude = state.altitude
1196 return vs < -8000 or vs > 8000 or \
1197 (altitude<500 and vs < (VSChecker.BELOW500 *
1198 VSChecker.TOLERANCE)) or \
1199 (altitude<2500 and vs < (VSChecker.BELOW2500 *
1200 VSChecker.TOLERANCE)) or \
1201 (altitude<5000 and vs < (VSChecker.BELOW5000 *
1202 VSChecker.TOLERANCE)) or \
1203 (altitude<10000 and vs < (VSChecker.BELOW10000 *
1204 VSChecker.TOLERANCE))
1205
1206 def logFault(self, flight, aircraft, logger, oldState, state):
1207 """Log the fault."""
1208 vs = state.smoothedVS
1209
1210 message = "Vertical speed was %.0f feet/min" % (vs,)
1211 if vs>-8000 and vs<8000:
1212 message += " at %.0f feet (exceeds company limit)" % (state.altitude,)
1213
1214 score = 10 if vs<-8000 or vs>8000 else 0
1215
1216 flight.handleFault(VSChecker, state.timestamp,
1217 FaultChecker._appendDuring(flight, message),
1218 score)
1219
1220#---------------------------------------------------------------------------------------
Note: See TracBrowser for help on using the repository browser.