source: src/mlx/flight.py@ 609:4eac92648123

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

Disconnection from the simulator is logged (re #249)

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