source: src/mlx/flight.py@ 564:695cde9818ea

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

The flight type is also considered when checking the arrival and departure times (re #227)

File size: 18.5 KB
RevLine 
[8]1
[176]2from soundsched import SoundScheduler, ChecklistScheduler
[349]3from checks import SpeedChecker
[170]4
[8]5import const
[89]6import util
[564]7import time
[8]8
9import threading
10
11#---------------------------------------------------------------------------------------
12
[298]13## @package mlx.flight
14#
15# The global flight state.
16#
17# This module defines a single class, \ref Flight, which represents the flight
18# in progress.
19
20#---------------------------------------------------------------------------------------
21
[8]22class Flight(object):
23 """The object with the global flight state.
[348]24
[8]25 It is also the hub for the other main objects participating in the handling of
26 the flight."""
[564]27
28 # The difference in minutes from the schedule which is considered a bit big
29 TIME_WARNING_DIFFERENCE = 5
30
31 # The difference in minutes from the schedule which is considered too big
32 TIME_ERROR_DIFFERENCE = 15
33
[383]34 @staticmethod
35 def canLogCruiseAltitude(stage):
36 """Determine if the cruise altitude can be logged in the given
37 stage."""
38 return stage in [const.STAGE_CRUISE, const.STAGE_DESCENT,
39 const.STAGE_LANDING]
40
[564]41 @staticmethod
42 def isTimeDifferenceTooMuch(scheduledTime, realTimestamp):
43 """Determine if the given real time differs to much from the scheduled
44 time.
45
46 Returns a tuple of:
47 - a boolean indicating if the difference is enough to warrant at least
48 a warning
49 - a boolean indicating if the difference is too big, i. e. unacceptable
50 without explanation."""
51 realTime = time.gmtime(realTimestamp)
52
53 scheduledMinute = scheduledTime.hour * 60 + scheduledTime.minute
54 realMinute = realTime.tm_hour * 60 + realTime.tm_min
55
56 diff1 = scheduledMinute - realMinute
57 diff2 = -1 * diff1
58
59 if diff1 < 0: diff1 += 60*24
60 else: diff2 += 60*24
61
62 diff = min(diff1, diff2)
63
64 return (diff>Flight.TIME_WARNING_DIFFERENCE,
65 diff>Flight.TIME_ERROR_DIFFERENCE)
66
[29]67 def __init__(self, logger, gui):
[8]68 """Construct the flight."""
69 self._stage = None
70 self.logger = logger
[29]71 self._gui = gui
[11]72
[31]73 gui.resetFlightStatus()
74
[170]75 self._pilotHotkeyPressed = False
76 self._checklistHotkeyPressed = False
77
[9]78 self.flareTimeFromFS = False
[11]79
[8]80 self.aircraftType = None
81 self.aircraft = None
82 self.simulator = None
83
[89]84 self.blockTimeStart = None
85 self.flightTimeStart = None
[365]86 self.climbTimeStart = None
87 self.cruiseTimeStart = None
88 self.descentTimeStart = None
[89]89 self.flightTimeEnd = None
90 self.blockTimeEnd = None
91
[349]92 self._rtoState = None
93 self._rtoLogEntryID = None
94
[89]95 self._lastDistanceTime = None
96 self._previousLatitude = None
97 self._previousLongitude = None
98 self.flownDistance = 0.0
99
100 self.startFuel = None
101 self.endFuel = None
102
[8]103 self._endCondition = threading.Condition()
104
[9]105 self._flareStart = None
106 self._flareStartFS = None
107
[170]108 self._tdRate = None
109
[176]110 self._soundScheduler = SoundScheduler(self)
111 self._checklistScheduler = ChecklistScheduler(self)
112
[8]113 @property
[134]114 def config(self):
115 """Get the configuration."""
116 return self._gui.config
117
118 @property
[8]119 def stage(self):
120 """Get the flight stage."""
121 return self._stage
122
[84]123 @property
[430]124 def fsType(self):
125 """Get the flight simulator type."""
126 return self._gui.fsType
127
128 @property
[215]129 def loggedIn(self):
130 """Indicate if the user has logged in properly."""
131 return self._gui.loggedIn
132
133 @property
[184]134 def entranceExam(self):
135 """Get whether an entrance exam is being performed."""
136 return self._gui.entranceExam
137
138 @property
[134]139 def bookedFlight(self):
140 """Get the booked flight."""
141 return self._gui.bookedFlight
142
143 @property
[303]144 def numCrew(self):
145 """Get the number of crew members on the flight."""
146 return self._gui.numCrew
147
148 @property
149 def numPassengers(self):
150 """Get the number of passengers on the flight."""
151 return self._gui.numPassengers
152
153 @property
154 def bagWeight(self):
155 """Get the baggage weight for the flight."""
156 return self._gui.bagWeight
157
158 @property
[262]159 def cargoWeight(self):
160 """Get the cargo weight for the flight."""
161 return self._gui.cargoWeight
162
163 @property
[303]164 def mailWeight(self):
165 """Get the mail weight for the flight."""
166 return self._gui.mailWeight
167
168 @property
[84]169 def zfw(self):
170 """Get the Zero-Fuel Weight of the flight."""
171 return self._gui.zfw
172
173 @property
[262]174 def filedCruiseAltitude(self):
175 """Get the filed cruise altitude."""
176 return self._gui.filedCruiseAltitude
177
178 @property
[84]179 def cruiseAltitude(self):
180 """Get the cruise altitude of the flight."""
181 return self._gui.cruiseAltitude
182
183 @property
[262]184 def route(self):
185 """Get the route of the flight."""
186 return self._gui.route
187
188 @property
189 def departureMETAR(self):
190 """Get the departure METAR of the flight."""
191 return self._gui.departureMETAR
192
193 @property
194 def arrivalMETAR(self):
195 """Get the arrival METAR of the flight."""
196 return self._gui.arrivalMETAR
197
198 @property
199 def departureRunway(self):
200 """Get the departure runway."""
201 return self._gui.departureRunway
202
203 @property
204 def sid(self):
205 """Get the SID followed."""
206 return self._gui.sid
207
208 @property
[84]209 def v1(self):
210 """Get the V1 speed of the flight."""
211 return self._gui.v1
212
213 @property
214 def vr(self):
215 """Get the Vr speed of the flight."""
216 return self._gui.vr
217
218 @property
219 def v2(self):
220 """Get the V2 speed of the flight."""
221 return self._gui.v2
222
[86]223 @property
[391]224 def takeoffAntiIceOn(self):
225 """Get whether the anti-ice system was on during takeoff."""
226 return self._gui.takeoffAntiIceOn
227
228 @takeoffAntiIceOn.setter
229 def takeoffAntiIceOn(self, value):
230 """Set whether the anti-ice system was on during takeoff."""
231 self._gui.takeoffAntiIceOn = value
232
233 @property
[384]234 def derate(self):
235 """Get the derate value of the flight."""
236 return self._gui.derate
237
238 @property
[262]239 def star(self):
240 """Get the STAR planned."""
241 return self._gui.star
242
243 @property
244 def transition(self):
245 """Get the transition planned."""
246 return self._gui.transition
247
248 @property
249 def approachType(self):
250 """Get the approach type."""
251 return self._gui.approachType
252
253 @property
254 def arrivalRunway(self):
255 """Get the arrival runway."""
256 return self._gui.arrivalRunway
257
258 @property
[86]259 def vref(self):
260 """Get the VRef speed of the flight."""
261 return self._gui.vref
262
[170]263 @property
[391]264 def landingAntiIceOn(self):
265 """Get whether the anti-ice system was on during landing."""
266 return self._gui.landingAntiIceOn
267
268 @landingAntiIceOn.setter
269 def landingAntiIceOn(self, value):
270 """Set whether the anti-ice system was on during landing."""
271 self._gui.landingAntiIceOn = value
272
273 @property
[170]274 def tdRate(self):
275 """Get the touchdown rate if known, None otherwise."""
276 return self._tdRate
277
[241]278 @property
[262]279 def flightType(self):
280 """Get the type of the flight."""
281 return self._gui.flightType
282
283 @property
284 def online(self):
285 """Get whether the flight was an online flight."""
286 return self._gui.online
287
288 @property
289 def comments(self):
290 """Get the comments made by the pilot."""
291 return self._gui.comments
292
293 @property
294 def flightDefects(self):
295 """Get the flight defects reported by the pilot."""
296 return self._gui.flightDefects
297
298 @property
299 def delayCodes(self):
300 """Get the delay codes."""
301 return self._gui.delayCodes
302
303 @property
[241]304 def speedInKnots(self):
305 """Determine if the speeds for the flight are to be expressed in
306 knots."""
307 return self.aircraft.speedInKnots if self.aircraft is not None \
308 else True
309
[349]310 @property
311 def hasRTO(self):
312 """Determine if we have an RTO state."""
313 return self._rtoState is not None
314
315 @property
316 def rtoState(self):
317 """Get the RTO state."""
318 return self._rtoState
319
[564]320 @property
321 def blockTimeStartWrong(self):
322 """Determine if the block time start is wrong compared to the scheduled
323 departure time.
324
325 Returns a tuple of:
326 - a boolean indicating if the difference warrants a warning
327 - a boolean indicating if the difference warrants not only a warning,
328 but an error as well."""
329 return self.isTimeDifferenceTooMuch(self.bookedFlight.departureTime,
330 self.blockTimeStart)
331
332 @property
333 def blockTimeEndWrong(self):
334 """Determine if the block time end is wrong compared to the scheduled
335 arrival time.
336
337 Returns a tuple of:
338 - a boolean indicating if the difference warrants a warning
339 - a boolean indicating if the difference warrants not only a warning,
340 but an error as well."""
341 return self.isTimeDifferenceTooMuch(self.bookedFlight.arrivalTime,
342 self.blockTimeEnd)
343
[89]344 def handleState(self, oldState, currentState):
345 """Handle a new state information."""
346 self._updateFlownDistance(currentState)
[348]347
[274]348 self.endFuel = currentState.totalFuel
[89]349 if self.startFuel is None:
350 self.startFuel = self.endFuel
[348]351
[170]352 self._soundScheduler.schedule(currentState,
353 self._pilotHotkeyPressed)
354 self._pilotHotkeyPressed = False
[89]355
[176]356 if self._checklistHotkeyPressed:
357 self._checklistScheduler.hotkeyPressed()
358 self._checklistHotkeyPressed = False
359
[8]360 def setStage(self, timestamp, stage):
[9]361 """Set the flight stage.
362
363 Returns if the stage has really changed."""
[8]364 if stage!=self._stage:
[365]365 self._logStageDuration(timestamp, stage)
[8]366 self._stage = stage
[84]367 self._gui.setStage(stage)
[8]368 self.logger.stage(timestamp, stage)
[89]369 if stage==const.STAGE_PUSHANDTAXI:
370 self.blockTimeStart = timestamp
371 elif stage==const.STAGE_TAKEOFF:
372 self.flightTimeStart = timestamp
[365]373 elif stage==const.STAGE_CLIMB:
374 self.climbTimeStart = timestamp
375 elif stage==const.STAGE_CRUISE:
376 self.cruiseTimeStart = timestamp
377 elif stage==const.STAGE_DESCENT:
378 self.descentTimeStart = timestamp
[89]379 elif stage==const.STAGE_TAXIAFTERLAND:
380 self.flightTimeEnd = timestamp
[359]381 # elif stage==const.STAGE_PARKING:
382 # self.blockTimeEnd = timestamp
383 elif stage==const.STAGE_END:
[89]384 self.blockTimeEnd = timestamp
[8]385 with self._endCondition:
386 self._endCondition.notify()
[9]387 return True
388 else:
389 return False
390
[346]391 def handleFault(self, faultID, timestamp, what, score,
[349]392 updatePrevious = False, updateID = None):
[30]393 """Handle the given fault.
394
395 faultID as a unique ID for the given kind of fault. If another fault of
396 this ID has been reported earlier, it will be reported again only if
397 the score is greater than last time. This ID can be, e.g. the checker
398 the report comes from."""
[349]399 id = self.logger.fault(faultID, timestamp, what, score,
400 updatePrevious = updatePrevious,
401 updateID = updateID)
[31]402 self._gui.setRating(self.logger.getRating())
[349]403 return id
[30]404
405 def handleNoGo(self, faultID, timestamp, what, shortReason):
406 """Handle a No-Go fault."""
407 self.logger.noGo(faultID, timestamp, what)
[31]408 self._gui.setNoGo(shortReason)
[30]409
[349]410 def setRTOState(self, state):
411 """Set the state that might be used as the RTO state.
412
413 If there has been no RTO state, the GUI is notified that from now on
414 the user may select to report an RTO."""
415 hadNoRTOState = self._rtoState is None
416
417 self._rtoState = state
418 self._rtoLogEntryID = \
419 SpeedChecker.logSpeedFault(self, state,
420 stage = const.STAGE_PUSHANDTAXI)
421
422 if hadNoRTOState:
423 self._gui.updateRTO()
424
425 def rtoToggled(self, indicated):
426 """Called when the user has toggled the RTO indication."""
427 if self._rtoState is not None:
428 if indicated:
429 self.logger.clearFault(self._rtoLogEntryID,
430 "RTO at %d knots" %
431 (self._rtoState.groundSpeed,))
432 self._gui.setRating(self.logger.getRating())
[354]433 if self._stage == const.STAGE_PUSHANDTAXI:
434 self.setStage(self.aircraft.state.timestamp,
435 const.STAGE_RTO)
[349]436 else:
437 SpeedChecker.logSpeedFault(self, self._rtoState,
438 stage = const.STAGE_PUSHANDTAXI,
439 updateID = self._rtoLogEntryID)
[354]440 if self._stage == const.STAGE_RTO:
441 self.setStage(self.aircraft.state.timestamp,
442 const.STAGE_PUSHANDTAXI)
[349]443
[9]444 def flareStarted(self, flareStart, flareStartFS):
445 """Called when the flare time has started."""
446 self._flareStart = flareStart
447 self._flareStartFS = flareStartFS
448
[170]449 def flareFinished(self, flareEnd, flareEndFS, tdRate):
[9]450 """Called when the flare time has ended.
[348]451
[9]452 Return a tuple of the following items:
453 - a boolean indicating if FS time is used
454 - the flare time
455 """
[170]456 self._tdRate = tdRate
[9]457 if self.flareTimeFromFS:
458 return (True, flareEndFS - self._flareStartFS)
459 else:
460 return (False, flareEnd - self._flareStart)
[8]461
462 def wait(self):
463 """Wait for the flight to end."""
464 with self._endCondition:
465 while self._stage!=const.STAGE_END:
[14]466 self._endCondition.wait(1)
[8]467
[134]468 def getFleet(self, callback, force = False):
469 """Get the fleet and call the given callback."""
470 self._gui.getFleetAsync(callback = callback, force = force)
471
[170]472 def pilotHotkeyPressed(self):
473 """Called when the pilot hotkey is pressed."""
474 self._pilotHotkeyPressed = True
475
476 def checklistHotkeyPressed(self):
477 """Called when the checklist hotkey is pressed."""
478 self._checklistHotkeyPressed = True
479
[241]480 def speedFromKnots(self, knots):
481 """Convert the given speed value expressed in knots into the flight's
482 speed unit."""
483 return knots if self.speedInKnots else knots * const.KNOTSTOKMPH
484
485 def speedToKnots(self, speed):
486 """Convert the given speed expressed in the flight's speed unit into
[348]487 knots."""
[241]488 return speed if self.speedInKnots else speed * const.KMPHTOKNOTS
489
490 def getEnglishSpeedUnit(self):
491 """Get the English name of the speed unit used by the flight."""
492 return "knots" if self.speedInKnots else "km/h"
493
494 def getI18NSpeedUnit(self):
495 """Get the speed unit suffix for i18n message identifiers."""
496 return "_knots" if self.speedInKnots else "_kmph"
497
[274]498 def logFuel(self, aircraftState):
499 """Log the amount of fuel"""
500 fuelStr = ""
501 for (tank, amount) in aircraftState.fuel:
502 if fuelStr: fuelStr += " - "
503 fuelStr += "%s=%.0f kg" % (const.fuelTank2logString(tank), amount)
[348]504
[274]505 self.logger.message(aircraftState.timestamp, "Fuel: " + fuelStr)
506 self.logger.message(aircraftState.timestamp,
[348]507 "Total fuel: %.0f kg" % (aircraftState.totalFuel,))
[274]508
[304]509 def cruiseLevelChanged(self):
510 """Called when the cruise level hass changed."""
[383]511 if self.canLogCruiseAltitude(self._stage):
[304]512 message = "Cruise altitude modified to %d feet" % \
[383]513 (self._gui.loggableCruiseAltitude,)
[304]514 self.logger.message(self.aircraft.timestamp, message)
[383]515 return True
516 else:
517 return False
[304]518
[89]519 def _updateFlownDistance(self, currentState):
520 """Update the flown distance."""
521 if not currentState.onTheGround:
522 updateData = False
523 if self._lastDistanceTime is None or \
524 self._previousLatitude is None or \
525 self._previousLongitude is None:
526 updateData = True
527 elif currentState.timestamp >= (self._lastDistanceTime + 30.0):
528 updateData = True
529 self.flownDistance += self._getDistance(currentState)
530
531 if updateData:
532 self._previousLatitude = currentState.latitude
533 self._previousLongitude = currentState.longitude
534 self._lastDistanceTime = currentState.timestamp
535 else:
536 if self._lastDistanceTime is not None and \
537 self._previousLatitude is not None and \
538 self._previousLongitude is not None:
539 self.flownDistance += self._getDistance(currentState)
[348]540
[89]541 self._lastDistanceTime = None
542
543 def _getDistance(self, currentState):
544 """Get the distance between the previous and the current state."""
545 return util.getDistCourse(self._previousLatitude, self._previousLongitude,
546 currentState.latitude, currentState.longitude)[0]
547
[365]548 def _logStageDuration(self, timestamp, stage):
549 """Log the duration of the stage preceding the given one."""
550 what = None
551 startTime = None
552
553 if stage==const.STAGE_TAKEOFF:
554 what = "Pushback and taxi"
555 startTime = self.blockTimeStart
556 elif stage==const.STAGE_CRUISE:
557 what = "Climb"
558 startTime = self.climbTimeStart
559 elif stage==const.STAGE_DESCENT:
560 what = "Cruise"
561 startTime = self.cruiseTimeStart
562 elif stage==const.STAGE_LANDING:
563 what = "Descent"
564 startTime = self.descentTimeStart
565 elif stage==const.STAGE_END:
566 what = "Taxi after landing"
567 startTime = self.flightTimeEnd
568
569 if what is not None and startTime is not None:
570 duration = timestamp - startTime
571 self.logger.message(timestamp,
572 "%s time: %s" % \
573 (what, util.getTimeIntervalString(duration)))
574
[8]575#---------------------------------------------------------------------------------------
Note: See TracBrowser for help on using the repository browser.