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
Line 
1
2from soundsched import SoundScheduler, ChecklistScheduler
3from checks import SpeedChecker
4
5import const
6import util
7import time
8
9import threading
10
11#---------------------------------------------------------------------------------------
12
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
22class Flight(object):
23 """The object with the global flight state.
24
25 It is also the hub for the other main objects participating in the handling of
26 the flight."""
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
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
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
67 def __init__(self, logger, gui):
68 """Construct the flight."""
69 self._stage = None
70 self.logger = logger
71 self._gui = gui
72
73 gui.resetFlightStatus()
74
75 self._pilotHotkeyPressed = False
76 self._checklistHotkeyPressed = False
77
78 self.flareTimeFromFS = False
79
80 self.aircraftType = None
81 self.aircraft = None
82 self.simulator = None
83
84 self.blockTimeStart = None
85 self.flightTimeStart = None
86 self.climbTimeStart = None
87 self.cruiseTimeStart = None
88 self.descentTimeStart = None
89 self.flightTimeEnd = None
90 self.blockTimeEnd = None
91
92 self._rtoState = None
93 self._rtoLogEntryID = None
94
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
103 self._endCondition = threading.Condition()
104
105 self._flareStart = None
106 self._flareStartFS = None
107
108 self._tdRate = None
109
110 self._soundScheduler = SoundScheduler(self)
111 self._checklistScheduler = ChecklistScheduler(self)
112
113 self._maxAltitude = 0
114
115 @property
116 def config(self):
117 """Get the configuration."""
118 return self._gui.config
119
120 @property
121 def stage(self):
122 """Get the flight stage."""
123 return self._stage
124
125 @property
126 def fsType(self):
127 """Get the flight simulator type."""
128 return self._gui.fsType
129
130 @property
131 def loggedIn(self):
132 """Indicate if the user has logged in properly."""
133 return self._gui.loggedIn
134
135 @property
136 def entranceExam(self):
137 """Get whether an entrance exam is being performed."""
138 return self._gui.entranceExam
139
140 @property
141 def bookedFlight(self):
142 """Get the booked flight."""
143 return self._gui.bookedFlight
144
145 @property
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
161 def cargoWeight(self):
162 """Get the cargo weight for the flight."""
163 return self._gui.cargoWeight
164
165 @property
166 def mailWeight(self):
167 """Get the mail weight for the flight."""
168 return self._gui.mailWeight
169
170 @property
171 def zfw(self):
172 """Get the Zero-Fuel Weight of the flight."""
173 return self._gui.zfw
174
175 @property
176 def filedCruiseAltitude(self):
177 """Get the filed cruise altitude."""
178 return self._gui.filedCruiseAltitude
179
180 @property
181 def cruiseAltitude(self):
182 """Get the cruise altitude of the flight."""
183 return self._gui.cruiseAltitude
184
185 @property
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
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
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
238 @property
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
249 def derate(self):
250 """Get the derate value of the flight."""
251 return self._gui.derate
252
253 @property
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
274 def vref(self):
275 """Get the VRef speed of the flight."""
276 return self._gui.vref
277
278 @property
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
289 def tdRate(self):
290 """Get the touchdown rate if known, None otherwise."""
291 return self._tdRate
292
293 @property
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
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
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
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
359 def handleState(self, oldState, currentState):
360 """Handle a new state information."""
361 self._updateFlownDistance(currentState)
362
363 self.endFuel = currentState.totalFuel
364 if self.startFuel is None:
365 self.startFuel = self.endFuel
366
367 self._soundScheduler.schedule(currentState,
368 self._pilotHotkeyPressed)
369 self._pilotHotkeyPressed = False
370
371 self._maxAltitude = max(currentState.altitude, self._maxAltitude)
372
373 if self._checklistHotkeyPressed:
374 self._checklistScheduler.hotkeyPressed()
375 self._checklistHotkeyPressed = False
376
377 def setStage(self, timestamp, stage):
378 """Set the flight stage.
379
380 Returns if the stage has really changed."""
381 if stage!=self._stage:
382 self._logStageDuration(timestamp, stage)
383 self._stage = stage
384 self._gui.setStage(stage)
385 self.logger.stage(timestamp, stage)
386 if stage==const.STAGE_PUSHANDTAXI:
387 self.blockTimeStart = timestamp
388 elif stage==const.STAGE_TAKEOFF:
389 self.flightTimeStart = timestamp
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
396 elif stage==const.STAGE_TAXIAFTERLAND:
397 self.flightTimeEnd = timestamp
398 # elif stage==const.STAGE_PARKING:
399 # self.blockTimeEnd = timestamp
400 elif stage==const.STAGE_END:
401 self.blockTimeEnd = timestamp
402 with self._endCondition:
403 self._endCondition.notify()
404 return True
405 else:
406 return False
407
408 def handleFault(self, faultID, timestamp, what, score,
409 updatePrevious = False, updateID = None):
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."""
416 id = self.logger.fault(faultID, timestamp, what, score,
417 updatePrevious = updatePrevious,
418 updateID = updateID)
419 self._gui.setRating(self.logger.getRating())
420 return id
421
422 def handleNoGo(self, faultID, timestamp, what, shortReason):
423 """Handle a No-Go fault."""
424 self.logger.noGo(faultID, timestamp, what)
425 self._gui.setNoGo(shortReason)
426
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())
450 if self._stage == const.STAGE_PUSHANDTAXI:
451 self.setStage(self.aircraft.state.timestamp,
452 const.STAGE_RTO)
453 else:
454 SpeedChecker.logSpeedFault(self, self._rtoState,
455 stage = const.STAGE_PUSHANDTAXI,
456 updateID = self._rtoLogEntryID)
457 if self._stage == const.STAGE_RTO:
458 self.setStage(self.aircraft.state.timestamp,
459 const.STAGE_PUSHANDTAXI)
460
461 def flareStarted(self, flareStart, flareStartFS):
462 """Called when the flare time has started."""
463 self._flareStart = flareStart
464 self._flareStartFS = flareStartFS
465
466 def flareFinished(self, flareEnd, flareEndFS, tdRate):
467 """Called when the flare time has ended.
468
469 Return a tuple of the following items:
470 - a boolean indicating if FS time is used
471 - the flare time
472 """
473 self._tdRate = tdRate
474 if self.flareTimeFromFS:
475 return (True, flareEndFS - self._flareStartFS)
476 else:
477 return (False, flareEnd - self._flareStart)
478
479 def wait(self):
480 """Wait for the flight to end."""
481 with self._endCondition:
482 while self._stage!=const.STAGE_END:
483 self._endCondition.wait(1)
484
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
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
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
504 knots."""
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
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)
521
522 self.logger.message(aircraftState.timestamp, "Fuel: " + fuelStr)
523 self.logger.message(aircraftState.timestamp,
524 "Total fuel: %.0f kg" % (aircraftState.totalFuel,))
525
526 def cruiseLevelChanged(self):
527 """Called when the cruise level hass changed."""
528 if self.canLogCruiseAltitude(self._stage):
529 message = "Cruise altitude modified to %d feet" % \
530 (self._gui.loggableCruiseAltitude,)
531 self.logger.message(self.aircraft.timestamp, message)
532 return True
533 else:
534 return False
535
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)
557
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
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
592#---------------------------------------------------------------------------------------
Note: See TracBrowser for help on using the repository browser.