source: src/mlx/checks.py@ 1098:cd3cf67ef749

python3
Last change on this file since 1098:cd3cf67ef749 was 1073:a5a22d24f890, checked in by István Váradi <ivaradi@…>, 2 years ago

New constant for X-Plane 12

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