source: src/mlx/flight.py@ 1157:4c335202dce1

python3
Last change on this file since 1157:4c335202dce1 was 1106:27adf311dd37, checked in by István Váradi <ivaradi@…>, 14 months ago

The flight contains if its departure gate is taxi-through

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