source: src/mlx/flight.py@ 708:2e411a2d77a0

Last change on this file since 708:2e411a2d77a0 was 635:717c097e0b12, checked in by István Váradi <ivaradi@…>, 9 years ago

The AGL altitude values are logged with aircraft type-specific units (re #264)

File size: 19.9 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
[635]326 def aglInFeet(self):
327 """Determine if the AGL altutides for the flight are to be expressed in
328 feet."""
329 return self.aircraft.aglInFeet if self.aircraft is not None \
330 else True
331
332 @property
[349]333 def hasRTO(self):
334 """Determine if we have an RTO state."""
335 return self._rtoState is not None
336
337 @property
338 def rtoState(self):
339 """Get the RTO state."""
340 return self._rtoState
341
[564]342 @property
343 def blockTimeStartWrong(self):
344 """Determine if the block time start is wrong compared to the scheduled
345 departure time.
346
347 Returns a tuple of:
348 - a boolean indicating if the difference warrants a warning
349 - a boolean indicating if the difference warrants not only a warning,
350 but an error as well."""
351 return self.isTimeDifferenceTooMuch(self.bookedFlight.departureTime,
352 self.blockTimeStart)
353
354 @property
355 def blockTimeEndWrong(self):
356 """Determine if the block time end is wrong compared to the scheduled
357 arrival time.
358
359 Returns a tuple of:
360 - a boolean indicating if the difference warrants a warning
361 - a boolean indicating if the difference warrants not only a warning,
362 but an error as well."""
363 return self.isTimeDifferenceTooMuch(self.bookedFlight.arrivalTime,
364 self.blockTimeEnd)
365
[609]366 def disconnected(self):
367 """Called when the connection to the simulator has failed."""
368 if self.aircraft is not None and self.aircraft.state is not None:
369 self.logger.message(self.aircraft.state.timestamp,
370 "The connection to the simulator has failed")
371
[89]372 def handleState(self, oldState, currentState):
373 """Handle a new state information."""
374 self._updateFlownDistance(currentState)
[348]375
[274]376 self.endFuel = currentState.totalFuel
[89]377 if self.startFuel is None:
378 self.startFuel = self.endFuel
[348]379
[170]380 self._soundScheduler.schedule(currentState,
381 self._pilotHotkeyPressed)
382 self._pilotHotkeyPressed = False
[89]383
[594]384 self._maxAltitude = max(currentState.altitude, self._maxAltitude)
385
[176]386 if self._checklistHotkeyPressed:
387 self._checklistScheduler.hotkeyPressed()
388 self._checklistHotkeyPressed = False
389
[8]390 def setStage(self, timestamp, stage):
[9]391 """Set the flight stage.
392
393 Returns if the stage has really changed."""
[8]394 if stage!=self._stage:
[365]395 self._logStageDuration(timestamp, stage)
[8]396 self._stage = stage
[84]397 self._gui.setStage(stage)
[8]398 self.logger.stage(timestamp, stage)
[89]399 if stage==const.STAGE_PUSHANDTAXI:
400 self.blockTimeStart = timestamp
401 elif stage==const.STAGE_TAKEOFF:
402 self.flightTimeStart = timestamp
[365]403 elif stage==const.STAGE_CLIMB:
404 self.climbTimeStart = timestamp
405 elif stage==const.STAGE_CRUISE:
406 self.cruiseTimeStart = timestamp
407 elif stage==const.STAGE_DESCENT:
408 self.descentTimeStart = timestamp
[89]409 elif stage==const.STAGE_TAXIAFTERLAND:
410 self.flightTimeEnd = timestamp
[359]411 # elif stage==const.STAGE_PARKING:
412 # self.blockTimeEnd = timestamp
413 elif stage==const.STAGE_END:
[89]414 self.blockTimeEnd = timestamp
[8]415 with self._endCondition:
416 self._endCondition.notify()
[9]417 return True
418 else:
419 return False
420
[346]421 def handleFault(self, faultID, timestamp, what, score,
[349]422 updatePrevious = False, updateID = None):
[30]423 """Handle the given fault.
424
425 faultID as a unique ID for the given kind of fault. If another fault of
426 this ID has been reported earlier, it will be reported again only if
427 the score is greater than last time. This ID can be, e.g. the checker
428 the report comes from."""
[349]429 id = self.logger.fault(faultID, timestamp, what, score,
430 updatePrevious = updatePrevious,
431 updateID = updateID)
[31]432 self._gui.setRating(self.logger.getRating())
[349]433 return id
[30]434
435 def handleNoGo(self, faultID, timestamp, what, shortReason):
436 """Handle a No-Go fault."""
437 self.logger.noGo(faultID, timestamp, what)
[31]438 self._gui.setNoGo(shortReason)
[30]439
[349]440 def setRTOState(self, state):
441 """Set the state that might be used as the RTO state.
442
443 If there has been no RTO state, the GUI is notified that from now on
444 the user may select to report an RTO."""
445 hadNoRTOState = self._rtoState is None
446
447 self._rtoState = state
448 self._rtoLogEntryID = \
449 SpeedChecker.logSpeedFault(self, state,
450 stage = const.STAGE_PUSHANDTAXI)
451
452 if hadNoRTOState:
453 self._gui.updateRTO()
454
455 def rtoToggled(self, indicated):
456 """Called when the user has toggled the RTO indication."""
457 if self._rtoState is not None:
458 if indicated:
459 self.logger.clearFault(self._rtoLogEntryID,
460 "RTO at %d knots" %
461 (self._rtoState.groundSpeed,))
462 self._gui.setRating(self.logger.getRating())
[354]463 if self._stage == const.STAGE_PUSHANDTAXI:
464 self.setStage(self.aircraft.state.timestamp,
465 const.STAGE_RTO)
[349]466 else:
467 SpeedChecker.logSpeedFault(self, self._rtoState,
468 stage = const.STAGE_PUSHANDTAXI,
469 updateID = self._rtoLogEntryID)
[354]470 if self._stage == const.STAGE_RTO:
471 self.setStage(self.aircraft.state.timestamp,
472 const.STAGE_PUSHANDTAXI)
[349]473
[9]474 def flareStarted(self, flareStart, flareStartFS):
475 """Called when the flare time has started."""
476 self._flareStart = flareStart
477 self._flareStartFS = flareStartFS
478
[170]479 def flareFinished(self, flareEnd, flareEndFS, tdRate):
[9]480 """Called when the flare time has ended.
[348]481
[9]482 Return a tuple of the following items:
483 - a boolean indicating if FS time is used
484 - the flare time
485 """
[170]486 self._tdRate = tdRate
[9]487 if self.flareTimeFromFS:
488 return (True, flareEndFS - self._flareStartFS)
489 else:
490 return (False, flareEnd - self._flareStart)
[8]491
492 def wait(self):
493 """Wait for the flight to end."""
494 with self._endCondition:
495 while self._stage!=const.STAGE_END:
[14]496 self._endCondition.wait(1)
[8]497
[134]498 def getFleet(self, callback, force = False):
499 """Get the fleet and call the given callback."""
500 self._gui.getFleetAsync(callback = callback, force = force)
501
[170]502 def pilotHotkeyPressed(self):
503 """Called when the pilot hotkey is pressed."""
504 self._pilotHotkeyPressed = True
505
506 def checklistHotkeyPressed(self):
507 """Called when the checklist hotkey is pressed."""
508 self._checklistHotkeyPressed = True
509
[241]510 def speedFromKnots(self, knots):
511 """Convert the given speed value expressed in knots into the flight's
512 speed unit."""
513 return knots if self.speedInKnots else knots * const.KNOTSTOKMPH
514
[635]515 def aglFromFeet(self, feet):
516 """Convert the given AGL altitude value expressed in feet into the
517 flight's AGL altitude unit."""
518 return feet if self.aglInFeet else feet * const.FEETTOMETRES
519
[241]520 def speedToKnots(self, speed):
521 """Convert the given speed expressed in the flight's speed unit into
[348]522 knots."""
[241]523 return speed if self.speedInKnots else speed * const.KMPHTOKNOTS
524
525 def getEnglishSpeedUnit(self):
526 """Get the English name of the speed unit used by the flight."""
527 return "knots" if self.speedInKnots else "km/h"
528
[635]529 def getEnglishAGLUnit(self):
530 """Get the English name of the AGL unit used by the flight."""
531 return "ft" if self.aglInFeet else "m"
532
[241]533 def getI18NSpeedUnit(self):
534 """Get the speed unit suffix for i18n message identifiers."""
535 return "_knots" if self.speedInKnots else "_kmph"
536
[274]537 def logFuel(self, aircraftState):
538 """Log the amount of fuel"""
539 fuelStr = ""
540 for (tank, amount) in aircraftState.fuel:
541 if fuelStr: fuelStr += " - "
542 fuelStr += "%s=%.0f kg" % (const.fuelTank2logString(tank), amount)
[348]543
[274]544 self.logger.message(aircraftState.timestamp, "Fuel: " + fuelStr)
545 self.logger.message(aircraftState.timestamp,
[348]546 "Total fuel: %.0f kg" % (aircraftState.totalFuel,))
[274]547
[304]548 def cruiseLevelChanged(self):
549 """Called when the cruise level hass changed."""
[383]550 if self.canLogCruiseAltitude(self._stage):
[304]551 message = "Cruise altitude modified to %d feet" % \
[383]552 (self._gui.loggableCruiseAltitude,)
[304]553 self.logger.message(self.aircraft.timestamp, message)
[383]554 return True
555 else:
556 return False
[304]557
[89]558 def _updateFlownDistance(self, currentState):
559 """Update the flown distance."""
560 if not currentState.onTheGround:
561 updateData = False
562 if self._lastDistanceTime is None or \
563 self._previousLatitude is None or \
564 self._previousLongitude is None:
565 updateData = True
566 elif currentState.timestamp >= (self._lastDistanceTime + 30.0):
567 updateData = True
568 self.flownDistance += self._getDistance(currentState)
569
570 if updateData:
571 self._previousLatitude = currentState.latitude
572 self._previousLongitude = currentState.longitude
573 self._lastDistanceTime = currentState.timestamp
574 else:
575 if self._lastDistanceTime is not None and \
576 self._previousLatitude is not None and \
577 self._previousLongitude is not None:
578 self.flownDistance += self._getDistance(currentState)
[348]579
[89]580 self._lastDistanceTime = None
581
582 def _getDistance(self, currentState):
583 """Get the distance between the previous and the current state."""
584 return util.getDistCourse(self._previousLatitude, self._previousLongitude,
585 currentState.latitude, currentState.longitude)[0]
586
[365]587 def _logStageDuration(self, timestamp, stage):
588 """Log the duration of the stage preceding the given one."""
589 what = None
590 startTime = None
591
592 if stage==const.STAGE_TAKEOFF:
593 what = "Pushback and taxi"
594 startTime = self.blockTimeStart
595 elif stage==const.STAGE_CRUISE:
596 what = "Climb"
597 startTime = self.climbTimeStart
598 elif stage==const.STAGE_DESCENT:
599 what = "Cruise"
600 startTime = self.cruiseTimeStart
601 elif stage==const.STAGE_LANDING:
602 what = "Descent"
603 startTime = self.descentTimeStart
604 elif stage==const.STAGE_END:
605 what = "Taxi after landing"
606 startTime = self.flightTimeEnd
607
608 if what is not None and startTime is not None:
609 duration = timestamp - startTime
610 self.logger.message(timestamp,
611 "%s time: %s" % \
612 (what, util.getTimeIntervalString(duration)))
613
[8]614#---------------------------------------------------------------------------------------
Note: See TracBrowser for help on using the repository browser.