source: src/mlx/flight.py@ 775:60bd73d51935

Last change on this file since 775:60bd73d51935 was 635:717c097e0b12, checked in by István Váradi <ivaradi@…>, 10 years ago

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

File size: 19.9 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 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
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
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
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
372 def handleState(self, oldState, currentState):
373 """Handle a new state information."""
374 self._updateFlownDistance(currentState)
375
376 self.endFuel = currentState.totalFuel
377 if self.startFuel is None:
378 self.startFuel = self.endFuel
379
380 self._soundScheduler.schedule(currentState,
381 self._pilotHotkeyPressed)
382 self._pilotHotkeyPressed = False
383
384 self._maxAltitude = max(currentState.altitude, self._maxAltitude)
385
386 if self._checklistHotkeyPressed:
387 self._checklistScheduler.hotkeyPressed()
388 self._checklistHotkeyPressed = False
389
390 def setStage(self, timestamp, stage):
391 """Set the flight stage.
392
393 Returns if the stage has really changed."""
394 if stage!=self._stage:
395 self._logStageDuration(timestamp, stage)
396 self._stage = stage
397 self._gui.setStage(stage)
398 self.logger.stage(timestamp, stage)
399 if stage==const.STAGE_PUSHANDTAXI:
400 self.blockTimeStart = timestamp
401 elif stage==const.STAGE_TAKEOFF:
402 self.flightTimeStart = timestamp
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
409 elif stage==const.STAGE_TAXIAFTERLAND:
410 self.flightTimeEnd = timestamp
411 # elif stage==const.STAGE_PARKING:
412 # self.blockTimeEnd = timestamp
413 elif stage==const.STAGE_END:
414 self.blockTimeEnd = timestamp
415 with self._endCondition:
416 self._endCondition.notify()
417 return True
418 else:
419 return False
420
421 def handleFault(self, faultID, timestamp, what, score,
422 updatePrevious = False, updateID = None):
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."""
429 id = self.logger.fault(faultID, timestamp, what, score,
430 updatePrevious = updatePrevious,
431 updateID = updateID)
432 self._gui.setRating(self.logger.getRating())
433 return id
434
435 def handleNoGo(self, faultID, timestamp, what, shortReason):
436 """Handle a No-Go fault."""
437 self.logger.noGo(faultID, timestamp, what)
438 self._gui.setNoGo(shortReason)
439
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())
463 if self._stage == const.STAGE_PUSHANDTAXI:
464 self.setStage(self.aircraft.state.timestamp,
465 const.STAGE_RTO)
466 else:
467 SpeedChecker.logSpeedFault(self, self._rtoState,
468 stage = const.STAGE_PUSHANDTAXI,
469 updateID = self._rtoLogEntryID)
470 if self._stage == const.STAGE_RTO:
471 self.setStage(self.aircraft.state.timestamp,
472 const.STAGE_PUSHANDTAXI)
473
474 def flareStarted(self, flareStart, flareStartFS):
475 """Called when the flare time has started."""
476 self._flareStart = flareStart
477 self._flareStartFS = flareStartFS
478
479 def flareFinished(self, flareEnd, flareEndFS, tdRate):
480 """Called when the flare time has ended.
481
482 Return a tuple of the following items:
483 - a boolean indicating if FS time is used
484 - the flare time
485 """
486 self._tdRate = tdRate
487 if self.flareTimeFromFS:
488 return (True, flareEndFS - self._flareStartFS)
489 else:
490 return (False, flareEnd - self._flareStart)
491
492 def wait(self):
493 """Wait for the flight to end."""
494 with self._endCondition:
495 while self._stage!=const.STAGE_END:
496 self._endCondition.wait(1)
497
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
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
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
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
520 def speedToKnots(self, speed):
521 """Convert the given speed expressed in the flight's speed unit into
522 knots."""
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
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
533 def getI18NSpeedUnit(self):
534 """Get the speed unit suffix for i18n message identifiers."""
535 return "_knots" if self.speedInKnots else "_kmph"
536
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)
543
544 self.logger.message(aircraftState.timestamp, "Fuel: " + fuelStr)
545 self.logger.message(aircraftState.timestamp,
546 "Total fuel: %.0f kg" % (aircraftState.totalFuel,))
547
548 def cruiseLevelChanged(self):
549 """Called when the cruise level hass changed."""
550 if self.canLogCruiseAltitude(self._stage):
551 message = "Cruise altitude modified to %d feet" % \
552 (self._gui.loggableCruiseAltitude,)
553 self.logger.message(self.aircraft.timestamp, message)
554 return True
555 else:
556 return False
557
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)
579
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
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
614#---------------------------------------------------------------------------------------
Note: See TracBrowser for help on using the repository browser.