source: src/mlx/flight.py@ 594:6e7e3e49f5dc

Last change on this file since 594:6e7e3e49f5dc was 594:6e7e3e49f5dc, checked in by István Váradi <ivaradi@…>, 9 years ago

Made the checking of the Descent stage a bit more clever (re #233)

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