source: src/mlx/checks.py@ 623:33edcb39a260

Last change on this file since 623:33edcb39a260 was 623:33edcb39a260, checked in by István Váradi <ivaradi@…>, 10 years ago

The strobeless speed checker is used for all aircraft to detect the takeoff stage (re #260)

File size: 68.4 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 BankChecker(SimpleFaultChecker):
1001 """Check for the bank is within limits."""
1002 def isCondition(self, flight, aircraft, oldState, state):
1003 """Check if the fault condition holds."""
1004 if flight.stage==const.STAGE_CRUISE:
1005 isDH8DXplane = flight.aircraftType==const.AIRCRAFT_DH8D and \
1006 (flight.fsType==const.SIM_XPLANE10 or
1007 flight.fsType==const.SIM_XPLANE9)
1008 bankLimit = 35 if isDH8DXplane else 30
1009 elif flight.stage in [const.STAGE_TAKEOFF, const.STAGE_CLIMB,
1010 const.STAGE_DESCENT, const.STAGE_LANDING]:
1011 bankLimit = 35
1012 else:
1013 return False
1014
1015 return state.bank>bankLimit or state.bank<-bankLimit
1016
1017 def logFault(self, flight, aircraft, logger, oldState, state):
1018 """Log the fault."""
1019 message = "Bank too steep (%.1f)" % (state.bank,)
1020 flight.handleFault(BankChecker, state.timestamp,
1021 FaultChecker._appendDuring(flight, message),
1022 2)
1023
1024#---------------------------------------------------------------------------------------
1025
1026class FlapsRetractChecker(SimpleFaultChecker):
1027 """Check if the flaps are not retracted too early."""
1028 def __init__(self):
1029 """Construct the flaps checker."""
1030 self._timeStart = None
1031
1032 def isCondition(self, flight, aircraft, oldState, state):
1033 """Check if the fault condition holds.
1034
1035 FIXME: check if this really is the intention (FlapsRetractedMistake.java)"""
1036 if (flight.stage==const.STAGE_TAKEOFF and not state.onTheGround and
1037 aircraft.type!=const.AIRCRAFT_F70) or \
1038 (flight.stage==const.STAGE_LANDING and state.onTheGround):
1039 if self._timeStart is None:
1040 self._timeStart = state.timestamp
1041
1042 if state.flapsSet==0 and state.timestamp<=(self._timeStart+2.0):
1043 return True
1044 else:
1045 self._timeStart = None
1046 return False
1047
1048 def logFault(self, flight, aircraft, logger, oldState, state):
1049 """Log the fault."""
1050 flight.handleFault(FlapsRetractChecker, state.timestamp,
1051 FaultChecker._appendDuring(flight, "Flaps retracted"),
1052 20)
1053
1054#---------------------------------------------------------------------------------------
1055
1056class FlapsSpeedLimitChecker(SimpleFaultChecker):
1057 """Check if the flaps are extended only at the right speeds."""
1058 def isCondition(self, flight, aircraft, oldState, state):
1059 """Check if the fault condition holds."""
1060 speedLimit = aircraft.getFlapsSpeedLimit(state.flapsSet)
1061 return speedLimit is not None and state.smoothedIAS>speedLimit
1062
1063 def logFault(self, flight, aircraft, logger, oldState, state):
1064 """Log the fault."""
1065 flight.handleFault(FlapsSpeedLimitChecker, state.timestamp,
1066 FaultChecker._appendDuring(flight, "Flap speed limit fault"),
1067 5)
1068
1069#---------------------------------------------------------------------------------------
1070
1071class GearsDownChecker(SimpleFaultChecker):
1072 """Check if the gears are down at low altitudes."""
1073 def isCondition(self, flight, aircraft, oldState, state):
1074 """Check if the fault condition holds."""
1075 return state.radioAltitude<10 and not state.gearsDown and \
1076 flight.stage!=const.STAGE_TAKEOFF
1077
1078 def logFault(self, flight, aircraft, logger, oldState, state):
1079 """Log the fault."""
1080 flight.handleNoGo(GearsDownChecker, state.timestamp,
1081 "Gears not down at %.0f feet radio altitude" % \
1082 (state.radioAltitude,),
1083 "GEAR DOWN NO GO")
1084
1085#---------------------------------------------------------------------------------------
1086
1087class GearSpeedLimitChecker(PatientFaultChecker):
1088 """Check if the gears not down at too high a speed."""
1089 def isCondition(self, flight, aircraft, oldState, state):
1090 """Check if the fault condition holds."""
1091 return state.gearsDown and state.smoothedIAS>aircraft.gearSpeedLimit
1092
1093 def logFault(self, flight, aircraft, logger, oldState, state):
1094 """Log the fault."""
1095 flight.handleFault(GearSpeedLimitChecker, state.timestamp,
1096 FaultChecker._appendDuring(flight, "Gear speed limit fault"),
1097 5)
1098
1099#---------------------------------------------------------------------------------------
1100
1101class GLoadChecker(SimpleFaultChecker):
1102 """Check if the G-load does not exceed 2 except during flare."""
1103 def isCondition(self, flight, aircraft, oldState, state):
1104 """Check if the fault condition holds."""
1105 return state.gLoad>2.0 and not state.onTheGround and \
1106 (flight.stage!=const.STAGE_LANDING or state.radioAltitude>=50)
1107
1108 def logFault(self, flight, aircraft, logger, oldState, state):
1109 """Log the fault."""
1110 flight.handleFault(GLoadChecker, state.timestamp,
1111 "G-load was %.2f" % (state.gLoad,),
1112 10)
1113
1114#---------------------------------------------------------------------------------------
1115
1116class LandingLightsChecker(PatientFaultChecker):
1117 """Check if the landing lights are used properly."""
1118 def getTimeout(self, flight, aircraft, oldState, state):
1119 """Get the timeout.
1120
1121 It is the default timeout except for landing and takeoff."""
1122 return 0.0 if flight.stage in [const.STAGE_TAKEOFF,
1123 const.STAGE_LANDING] else self._timeout
1124
1125 def isCondition(self, flight, aircraft, oldState, state):
1126 """Check if the fault condition holds."""
1127 return state.landingLightsOn is not None and \
1128 ((flight.stage==const.STAGE_BOARDING and \
1129 state.landingLightsOn and state.onTheGround) or \
1130 (flight.stage==const.STAGE_TAKEOFF and \
1131 not state.landingLightsOn and not state.onTheGround) or \
1132 (flight.stage in [const.STAGE_CLIMB, const.STAGE_CRUISE,
1133 const.STAGE_DESCENT] and \
1134 state.landingLightsOn and state.altitude>12500) or \
1135 (flight.stage==const.STAGE_LANDING and \
1136 not state.landingLightsOn and state.onTheGround and
1137 state.groundSpeed>50.0) or \
1138 (flight.stage==const.STAGE_PARKING and \
1139 state.landingLightsOn and state.onTheGround))
1140
1141 def logFault(self, flight, aircraft, logger, oldState, state):
1142 """Log the fault."""
1143 score = 0 if flight.stage in [const.STAGE_TAKEOFF,
1144 const.STAGE_LANDING] else 1
1145 message = "Landing lights were %s" % \
1146 (("on" if state.landingLightsOn else "off"),)
1147 flight.handleFault(LandingLightsChecker, state.timestamp,
1148 FaultChecker._appendDuring(flight, message),
1149 score)
1150
1151#---------------------------------------------------------------------------------------
1152
1153class TransponderChecker(PatientFaultChecker):
1154 """Check if the transponder is used properly."""
1155 def __init__(self):
1156 """Construct the transponder checker."""
1157 super(TransponderChecker, self).__init__()
1158 self._liftOffTime = None
1159
1160 def isCondition(self, flight, aircraft, oldState, state):
1161 """Check if the fault condition holds."""
1162 if state.onTheGround:
1163 self._liftOffTime = None
1164 elif self._liftOffTime is None:
1165 self._liftOffTime = state.timestamp
1166
1167 return state.xpdrC is not None and \
1168 ((state.xpdrC and flight.stage in
1169 [const.STAGE_BOARDING, const.STAGE_PARKING]) or \
1170 (not state.xpdrC and
1171 (flight.stage in
1172 [const.STAGE_CRUISE, const.STAGE_DESCENT,
1173 const.STAGE_GOAROUND] or \
1174 (flight.stage==const.STAGE_LANDING and
1175 state.groundSpeed>50.0) or \
1176 ((not state.autoXPDR or \
1177 (self._liftOffTime is not None and
1178 state.timestamp > (self._liftOffTime+8))) and \
1179 ((flight.stage==const.STAGE_TAKEOFF and
1180 not state.onTheGround) or flight.stage==const.STAGE_CLIMB))
1181 )
1182 )
1183 )
1184
1185 def logFault(self, flight, aircraft, logger, oldState, state):
1186 """Log the fault."""
1187 score = 0
1188 message = "Transponder was %s" % \
1189 (("mode C" if state.xpdrC else "standby"),)
1190 flight.handleFault(TransponderChecker, state.timestamp,
1191 FaultChecker._appendDuring(flight, message),
1192 score)
1193
1194#---------------------------------------------------------------------------------------
1195
1196class WeightChecker(PatientFaultChecker):
1197 """Base class for checkers that check that some limit is not exceeded."""
1198 def __init__(self, name):
1199 """Construct the checker."""
1200 super(WeightChecker, self).__init__(timeout = 5.0)
1201 self._name = name
1202
1203 def isCondition(self, flight, aircraft, oldState, state):
1204 """Check if the fault condition holds."""
1205 if flight.entranceExam:
1206 return False
1207
1208 limit = self.getLimit(flight, aircraft, state)
1209 if limit is not None:
1210 #if flight.options.compensation is not None:
1211 # limit += flight.options.compensation
1212 return self.getWeight(state)>limit
1213
1214 return False
1215
1216 def logFault(self, flight, aircraft, logger, oldState, state):
1217 """Log the fault."""
1218 mname = "M" + self._name
1219 flight.handleNoGo(self.__class__, state.timestamp,
1220 "%s exceeded: %s is %.0f kg" % \
1221 (mname, self._name, self.getWeight(state)),
1222 "%s NO GO" % (mname,))
1223
1224 def getWeight(self, state):
1225 """Get the weight that is interesting for us."""
1226 return state.grossWeight
1227
1228#---------------------------------------------------------------------------------------
1229
1230class MLWChecker(WeightChecker):
1231 """Checks if the MLW is not exceeded on landing."""
1232 def __init__(self):
1233 """Construct the checker."""
1234 super(MLWChecker, self).__init__("LW")
1235
1236 def getLimit(self, flight, aircraft, state):
1237 """Get the limit if we are in the right state."""
1238 return aircraft.mlw if flight.stage==const.STAGE_LANDING and \
1239 state.onTheGround and \
1240 not flight.entranceExam else None
1241
1242#---------------------------------------------------------------------------------------
1243
1244class MTOWChecker(WeightChecker):
1245 """Checks if the MTOW is not exceeded on landing."""
1246 def __init__(self):
1247 """Construct the checker."""
1248 super(MTOWChecker, self).__init__("TOW")
1249
1250 def getLimit(self, flight, aircraft, state):
1251 """Get the limit if we are in the right state."""
1252 return aircraft.mtow if flight.stage==const.STAGE_TAKEOFF and \
1253 not flight.entranceExam else None
1254
1255#---------------------------------------------------------------------------------------
1256
1257class MZFWChecker(WeightChecker):
1258 """Checks if the MZFW is not exceeded on landing."""
1259 def __init__(self):
1260 """Construct the checker."""
1261 super(MZFWChecker, self).__init__("ZFW")
1262
1263 def getLimit(self, flight, aircraft, state):
1264 """Get the limit if we are in the right state."""
1265 return aircraft.mzfw if not flight.entranceExam else None
1266
1267 def getWeight(self, state):
1268 """Get the weight that is interesting for us."""
1269 return state.zfw
1270
1271#---------------------------------------------------------------------------------------
1272
1273class NavLightsChecker(PatientFaultChecker):
1274 """Check if the navigational lights are used properly."""
1275 def isCondition(self, flight, aircraft, oldState, state):
1276 """Check if the fault condition holds."""
1277 return flight.stage!=const.STAGE_BOARDING and \
1278 flight.stage!=const.STAGE_PARKING and \
1279 state.navLightsOn is False
1280
1281 def logFault(self, flight, aircraft, logger, oldState, state):
1282 """Log the fault."""
1283 flight.handleFault(NavLightsChecker, state.timestamp,
1284 FaultChecker._appendDuring(flight,
1285 "Navigation lights were off"),
1286 1)
1287
1288#---------------------------------------------------------------------------------------
1289
1290class WindSensitiveFaultChecker(FaultChecker):
1291 """A fault checker which checks for a fault condition that might arise due
1292 to suddenly changing winds.
1293
1294 If the condition is detected, its fact is logged, and the winds are also
1295 logged repeatedly until and for a while after the condition has ceased. A
1296 fault is logged only if the condition holds for a certain period of
1297 time."""
1298 # The time the winds were logged last. This is global to avoid duplicate
1299 # logging if several wind-sensitive fault conditions gold
1300 _windsLastLogged = None
1301
1302 # The wind logging interval
1303 _windsLogInterval = 5.0
1304
1305 def __init__(self, faultTimeout = 30.0, afterLoggingTime = 20.0):
1306 """Construct the fault checker."""
1307 self._faultTimeout = faultTimeout
1308 self._afterLoggingTime = afterLoggingTime
1309
1310 self._faultStarted = None
1311 self._faultCeased = None
1312
1313 def check(self, flight, aircraft, logger, oldState, state):
1314 """Check for the condition and do whatever is needed."""
1315 timestamp = state.timestamp
1316 logWinds = False
1317
1318 if self.isCondition(flight, aircraft, oldState, state):
1319 logWinds = True
1320 if self._faultStarted is None:
1321 self._faultStarted = timestamp
1322 self._faultCeased = None
1323 self.logCondition(flight, aircraft, logger, oldState, state,
1324 False)
1325
1326 WindSensitiveFaultChecker._windsLastLogged = None
1327
1328 elif timestamp >= (self._faultStarted + self._faultTimeout):
1329 self.logCondition(flight, aircraft, logger, oldState, state,
1330 True)
1331 self._faultStarted = timestamp
1332
1333 else:
1334 if self._faultStarted is not None and self._faultCeased is None:
1335 self._faultCeased = timestamp
1336 self._faultStarted = None
1337
1338 if self._faultCeased is not None:
1339 if timestamp < (self._faultCeased + self._afterLoggingTime):
1340 logWinds = True
1341 else:
1342 self._faultCeased = None
1343
1344 if logWinds and \
1345 (WindSensitiveFaultChecker._windsLastLogged is None or
1346 timestamp >= (WindSensitiveFaultChecker._windsLastLogged +
1347 self._windsLogInterval)):
1348 logger.message(timestamp, "Winds: %.f knots from %.f" %
1349 (state.windSpeed, state.windDirection))
1350 WindSensitiveFaultChecker._windsLastLogged = timestamp
1351
1352#---------------------------------------------------------------------------------------
1353
1354class OverspeedChecker(WindSensitiveFaultChecker):
1355 """Check if Vne has been exceeded."""
1356 def isCondition(self, flight, aircraft, oldState, state):
1357 """Check if the fault condition holds."""
1358 return state.overspeed
1359
1360 def logCondition(self, flight, aircraft, logger, oldState, state, isFault):
1361 """Log the condition fault."""
1362 if isFault:
1363 flight.handleFault(OverspeedChecker, state.timestamp,
1364 FaultChecker._appendDuring(flight, "Overspeed"),
1365 20)
1366 else:
1367 logger.message(state.timestamp,
1368 FaultChecker._appendDuring(flight, "Overspeed"))
1369
1370#---------------------------------------------------------------------------------------
1371
1372class StallChecker(WindSensitiveFaultChecker):
1373 """Check if stall occured."""
1374 def isCondition(self, flight, aircraft, oldState, state):
1375 """Check if the fault condition holds."""
1376 return flight.stage in [const.STAGE_TAKEOFF, const.STAGE_CLIMB,
1377 const.STAGE_CRUISE, const.STAGE_DESCENT,
1378 const.STAGE_LANDING] and state.stalled
1379
1380 def logCondition(self, flight, aircraft, logger, oldState, state, isFault):
1381 """Log the condition."""
1382 if isFault:
1383 score = 40 if flight.stage in [const.STAGE_TAKEOFF,
1384 const.STAGE_LANDING] else 30
1385 flight.handleFault(StallChecker, state.timestamp,
1386 FaultChecker._appendDuring(flight, "Stalled"),
1387 score)
1388 else:
1389 logger.message(state.timestamp,
1390 FaultChecker._appendDuring(flight, "Stalled"))
1391
1392#---------------------------------------------------------------------------------------
1393
1394class PayloadChecker(SimpleFaultChecker):
1395 """Check if the payload matches the specification."""
1396 TOLERANCE=550
1397
1398 @staticmethod
1399 def isZFWFaulty(aircraftZFW, flightZFW):
1400 """Check if the given aircraft's ZFW is outside of the limits."""
1401 return aircraftZFW < (flightZFW - PayloadChecker.TOLERANCE) or \
1402 aircraftZFW > (flightZFW + PayloadChecker.TOLERANCE)
1403
1404 def isCondition(self, flight, aircraft, oldState, state):
1405 """Check if the fault condition holds."""
1406 return not flight.entranceExam and \
1407 flight.stage==const.STAGE_PUSHANDTAXI and \
1408 PayloadChecker.isZFWFaulty(state.zfw, flight.zfw)
1409
1410 def logFault(self, flight, aircraft, logger, oldState, state):
1411 """Log the fault."""
1412 flight.handleNoGo(PayloadChecker, state.timestamp,
1413 "ZFW difference is more than %d kgs" % \
1414 (PayloadChecker.TOLERANCE,),
1415 "ZFW NO GO")
1416
1417#---------------------------------------------------------------------------------------
1418
1419class PitotChecker(PatientFaultChecker):
1420 """Check if pitot heat is on."""
1421 def __init__(self):
1422 """Construct the checker."""
1423 super(PitotChecker, self).__init__(timeout = 3.0)
1424
1425 def isCondition(self, flight, aircraft, oldState, state):
1426 """Check if the fault condition holds."""
1427 return state.groundSpeed>80 and not state.pitotHeatOn
1428
1429 def logFault(self, flight, aircraft, logger, oldState, state):
1430 """Log the fault."""
1431 score = 2 if flight.stage in [const.STAGE_TAKEOFF, const.STAGE_CLIMB,
1432 const.STAGE_CRUISE, const.STAGE_DESCENT,
1433 const.STAGE_LANDING] else 0
1434 flight.handleFault(PitotChecker, state.timestamp,
1435 FaultChecker._appendDuring(flight, "Pitot heat was off"),
1436 score)
1437
1438#---------------------------------------------------------------------------------------
1439
1440class ReverserLogger(StateChecker):
1441 """Logger for the reverser."""
1442 def check(self, flight, aircraft, logger, oldState, state):
1443 """Log the cruise speed if necessary."""
1444 if oldState is not None and state.reverser != oldState.reverser:
1445 reverser = max(state.reverser)
1446 logger.message(state.timestamp,
1447 "Reverser " + ("unlocked" if reverser else "closed") +
1448 (" - %.0f %s" % (flight.speedFromKnots(state.ias),
1449 flight.getEnglishSpeedUnit())))
1450
1451#---------------------------------------------------------------------------------------
1452
1453class ReverserChecker(SimpleFaultChecker):
1454 """Check if the reverser is not used below the speed prescribed for the
1455 aircraft."""
1456 def isCondition(self, flight, aircraft, oldState, state):
1457 """Check if the fault condition holds."""
1458 return flight.stage in [const.STAGE_DESCENT, const.STAGE_LANDING,
1459 const.STAGE_TAXIAFTERLAND] and \
1460 state.reverser and \
1461 state.ias<aircraft.reverseMinSpeed and max(state.reverser)
1462
1463 def logFault(self, flight, aircraft, logger, oldState, state):
1464 """Log the fault."""
1465 message = "Reverser used below %.0f %s" % \
1466 (flight.speedFromKnots(aircraft.reverseMinSpeed), flight.getEnglishSpeedUnit())
1467 flight.handleFault(ReverserChecker, state.timestamp,
1468 FaultChecker._appendDuring(flight, message),
1469 15)
1470
1471#---------------------------------------------------------------------------------------
1472
1473class SpeedChecker(StateChecker):
1474 """Checker for the ground speed of aircraft.
1475
1476 If, during the PUSHANDTAXI stage the speed exceeds 50 knots, the state is
1477 saved as a provisional takeoff state. If the speed then decreases below 50
1478 knots, or the plane remains on the ground for more than 40 seconds, a taxi
1479 speed error is logged with the highest ground speed detected. This state is
1480 also stored in the flight object as a possible
1481
1482 If the plane becomes airborne within 40 seconds, the stage becomes TAKEOFF,
1483 and the previously saved takeoff state is logged.
1484
1485 During the TAXIAFTERLAND stage, if the speed goes above 50 knots, a fault
1486 is logged based on the actual speed."""
1487 @staticmethod
1488 def logSpeedFault(flight, state, stage = None, updateID = None):
1489 """Log the speed fault."""
1490 message = "Taxi speed over %.0f %s" % \
1491 (flight.speedFromKnots(50), flight.getEnglishSpeedUnit())
1492 if stage is None:
1493 stage = flight.stage
1494 score = FaultChecker._getLinearScore(50, 80, 10, 15, state.groundSpeed)
1495 return flight.handleFault((SpeedChecker, stage), state.timestamp,
1496 FaultChecker._appendDuring(flight, message),
1497 score, updatePrevious = updateID is None,
1498 updateID = updateID)
1499
1500 def __init__(self):
1501 """Initialize the speed checker."""
1502 self._takeoffState = None
1503 self._highestSpeedState = None
1504
1505 def check(self, flight, aircraft, logger, oldState, state):
1506 """Check the state as described above."""
1507 if flight.stage==const.STAGE_PUSHANDTAXI or \
1508 flight.stage==const.STAGE_RTO:
1509 self._checkPushAndTaxi(flight, aircraft, state)
1510 elif flight.stage==const.STAGE_TAXIAFTERLAND:
1511 if state.groundSpeed>50:
1512 self.logSpeedFault(flight, state)
1513 else:
1514 self._takeoffState = None
1515
1516 def _checkPushAndTaxi(self, flight, aircraft, state):
1517 """Check the speed during the push and taxi stage."""
1518 if state.groundSpeed>50:
1519 if self._takeoffState is None:
1520 self._takeoffState = state
1521 self._highestSpeedState = state
1522 else:
1523 if state.groundSpeed>self._highestSpeedState.groundSpeed:
1524 self._highestSpeedState = state
1525
1526 if not state.onTheGround:
1527 aircraft.setStage(self._takeoffState, const.STAGE_TAKEOFF)
1528 self._takeoffState = None
1529 elif state.timestamp > (self._takeoffState.timestamp + 40):
1530 flight.setRTOState(self._highestSpeedState)
1531 elif self._takeoffState is not None:
1532 flight.setRTOState(self._highestSpeedState)
1533 self._takeoffState = None
1534
1535#---------------------------------------------------------------------------------------
1536
1537class StrobeLightsChecker(PatientFaultChecker):
1538 """Check if the strobe lights are used properly."""
1539 def isCondition(self, flight, aircraft, oldState, state):
1540 """Check if the fault condition holds."""
1541 return state.strobeLightsOn is not None and \
1542 ((flight.stage==const.STAGE_BOARDING and \
1543 state.strobeLightsOn and state.onTheGround) or \
1544 (flight.stage==const.STAGE_TAKEOFF and \
1545 not state.strobeLightsOn and not state.gearsDown) or \
1546 (flight.stage in [const.STAGE_CLIMB, const.STAGE_CRUISE,
1547 const.STAGE_DESCENT] and \
1548 not state.strobeLightsOn and not state.onTheGround) or \
1549 (flight.stage==const.STAGE_PARKING and \
1550 state.strobeLightsOn and state.onTheGround))
1551
1552 def logFault(self, flight, aircraft, logger, oldState, state):
1553 """Log the fault."""
1554 message = "Strobe lights were %s" % (("on" if state.strobeLightsOn else "off"),)
1555 flight.handleFault(StrobeLightsChecker, state.timestamp,
1556 FaultChecker._appendDuring(flight, message),
1557 1)
1558
1559#---------------------------------------------------------------------------------------
1560
1561class ThrustChecker(SimpleFaultChecker):
1562 """Check if the thrust setting is not too high during takeoff.
1563
1564 FIXME: is this really so general, for all aircraft?"""
1565 def isCondition(self, flight, aircraft, oldState, state):
1566 """Check if the fault condition holds."""
1567 return flight.stage==const.STAGE_TAKEOFF and \
1568 state.n1 is not None and max(state.n1)>97
1569
1570 def logFault(self, flight, aircraft, logger, oldState, state):
1571 """Log the fault."""
1572 flight.handleFault(ThrustChecker, state.timestamp,
1573 FaultChecker._appendDuring(flight,
1574 "Thrust setting was too high (>97%)"),
1575 FaultChecker._getLinearScore(97, 110, 0, 10,
1576 max(state.n1)),
1577 updatePrevious = True)
1578
1579#---------------------------------------------------------------------------------------
1580
1581class VSChecker(SimpleFaultChecker):
1582 """Check if the vertical speed is not too low at certain altitudes"""
1583 BELOW10000 = -5000
1584 BELOW5000 = -2500
1585 BELOW2500 = -1500
1586 BELOW500 = -1000
1587 TOLERANCE = 1.2
1588
1589 def isCondition(self, flight, aircraft, oldState, state):
1590 """Check if the fault condition holds."""
1591 vs = state.smoothedVS
1592 altitude = state.altitude
1593 return vs < -8000 or vs > 8000 or \
1594 (altitude<500 and vs < (VSChecker.BELOW500 *
1595 VSChecker.TOLERANCE)) or \
1596 (altitude<2500 and vs < (VSChecker.BELOW2500 *
1597 VSChecker.TOLERANCE)) or \
1598 (altitude<5000 and vs < (VSChecker.BELOW5000 *
1599 VSChecker.TOLERANCE)) or \
1600 (altitude<10000 and vs < (VSChecker.BELOW10000 *
1601 VSChecker.TOLERANCE))
1602
1603 def logFault(self, flight, aircraft, logger, oldState, state):
1604 """Log the fault."""
1605 vs = state.smoothedVS
1606
1607 message = "Vertical speed was %.0f feet/min" % (vs,)
1608 if vs>-8000 and vs<8000:
1609 message += " at %.0f feet (exceeds company limit)" % (state.altitude,)
1610
1611 score = 10 if vs<-8000 or vs>8000 else 0
1612
1613 flight.handleFault(VSChecker, state.timestamp,
1614 FaultChecker._appendDuring(flight, message),
1615 score)
1616
1617#---------------------------------------------------------------------------------------
Note: See TracBrowser for help on using the repository browser.