source: src/mlx/flight.py

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

The flight contains if its departure gate is taxi-through

File size: 20.9 KB
RevLine 
[8]1
[919]2from .soundsched import SoundScheduler, ChecklistScheduler
3from .checks import SpeedChecker
[170]4
[919]5from . import const
6from . import 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
[846]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
[1034]56 def isTimeDifferenceTooMuch(scheduledTime, realTimestamp,
57 earlyOnlyWarning = False):
[564]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
[846]71 diff = abs(Flight.getMinutesDifference(scheduledMinute, realMinute))
[564]72
73 return (diff>Flight.TIME_WARNING_DIFFERENCE,
[1034]74 False if earlyOnlyWarning else diff>Flight.TIME_ERROR_DIFFERENCE)
[564]75
[29]76 def __init__(self, logger, gui):
[8]77 """Construct the flight."""
78 self._stage = None
79 self.logger = logger
[29]80 self._gui = gui
[11]81
[31]82 gui.resetFlightStatus()
83
[170]84 self._pilotHotkeyPressed = False
85 self._checklistHotkeyPressed = False
86
[9]87 self.flareTimeFromFS = False
[11]88
[8]89 self.aircraftType = None
90 self.aircraft = None
91 self.simulator = None
92
[89]93 self.blockTimeStart = None
94 self.flightTimeStart = None
[365]95 self.climbTimeStart = None
96 self.cruiseTimeStart = None
97 self.descentTimeStart = None
[89]98 self.flightTimeEnd = None
99 self.blockTimeEnd = None
100
[349]101 self._rtoState = None
102 self._rtoLogEntryID = None
103
[89]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
[8]112 self._endCondition = threading.Condition()
113
[9]114 self._flareStart = None
115 self._flareStartFS = None
116
[170]117 self._tdRate = None
118
[176]119 self._soundScheduler = SoundScheduler(self)
120 self._checklistScheduler = ChecklistScheduler(self)
121
[594]122 self._maxAltitude = 0
123
[1106]124 self.departureGateIsTaxiThrough = True
125
[8]126 @property
[134]127 def config(self):
128 """Get the configuration."""
129 return self._gui.config
130
131 @property
[8]132 def stage(self):
133 """Get the flight stage."""
134 return self._stage
135
[84]136 @property
[430]137 def fsType(self):
138 """Get the flight simulator type."""
139 return self._gui.fsType
140
141 @property
[215]142 def loggedIn(self):
143 """Indicate if the user has logged in properly."""
144 return self._gui.loggedIn
145
146 @property
[184]147 def entranceExam(self):
148 """Get whether an entrance exam is being performed."""
149 return self._gui.entranceExam
150
151 @property
[134]152 def bookedFlight(self):
153 """Get the booked flight."""
154 return self._gui.bookedFlight
155
156 @property
[1033]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
[303]165
166 @property
167 def numPassengers(self):
168 """Get the number of passengers on the flight."""
169 return self._gui.numPassengers
170
171 @property
[1033]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
[303]182 def bagWeight(self):
183 """Get the baggage weight for the flight."""
184 return self._gui.bagWeight
185
186 @property
[262]187 def cargoWeight(self):
188 """Get the cargo weight for the flight."""
189 return self._gui.cargoWeight
190
191 @property
[303]192 def mailWeight(self):
193 """Get the mail weight for the flight."""
194 return self._gui.mailWeight
195
196 @property
[84]197 def zfw(self):
198 """Get the Zero-Fuel Weight of the flight."""
199 return self._gui.zfw
200
201 @property
[262]202 def filedCruiseAltitude(self):
203 """Get the filed cruise altitude."""
204 return self._gui.filedCruiseAltitude
205
206 @property
[84]207 def cruiseAltitude(self):
208 """Get the cruise altitude of the flight."""
209 return self._gui.cruiseAltitude
210
211 @property
[594]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
[262]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
[84]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
[86]264 @property
[391]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
[384]275 def derate(self):
276 """Get the derate value of the flight."""
277 return self._gui.derate
278
279 @property
[262]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
[86]300 def vref(self):
301 """Get the VRef speed of the flight."""
302 return self._gui.vref
303
[170]304 @property
[391]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
[170]315 def tdRate(self):
316 """Get the touchdown rate if known, None otherwise."""
317 return self._tdRate
318
[241]319 @property
[262]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
[241]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
[349]351 @property
[635]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
[349]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
[564]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,
[1034]390 self.blockTimeEnd,
391 earlyOnlyWarning = True)
[564]392
[609]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
[89]399 def handleState(self, oldState, currentState):
400 """Handle a new state information."""
401 self._updateFlownDistance(currentState)
[348]402
[274]403 self.endFuel = currentState.totalFuel
[89]404 if self.startFuel is None:
405 self.startFuel = self.endFuel
[348]406
[170]407 self._soundScheduler.schedule(currentState,
408 self._pilotHotkeyPressed)
409 self._pilotHotkeyPressed = False
[89]410
[594]411 self._maxAltitude = max(currentState.altitude, self._maxAltitude)
412
[176]413 if self._checklistHotkeyPressed:
414 self._checklistScheduler.hotkeyPressed()
415 self._checklistHotkeyPressed = False
416
[8]417 def setStage(self, timestamp, stage):
[9]418 """Set the flight stage.
419
420 Returns if the stage has really changed."""
[8]421 if stage!=self._stage:
[365]422 self._logStageDuration(timestamp, stage)
[8]423 self._stage = stage
[84]424 self._gui.setStage(stage)
[8]425 self.logger.stage(timestamp, stage)
[89]426 if stage==const.STAGE_PUSHANDTAXI:
427 self.blockTimeStart = timestamp
428 elif stage==const.STAGE_TAKEOFF:
429 self.flightTimeStart = timestamp
[365]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
[89]436 elif stage==const.STAGE_TAXIAFTERLAND:
437 self.flightTimeEnd = timestamp
[359]438 # elif stage==const.STAGE_PARKING:
439 # self.blockTimeEnd = timestamp
440 elif stage==const.STAGE_END:
[89]441 self.blockTimeEnd = timestamp
[8]442 with self._endCondition:
443 self._endCondition.notify()
[9]444 return True
445 else:
446 return False
447
[346]448 def handleFault(self, faultID, timestamp, what, score,
[349]449 updatePrevious = False, updateID = None):
[30]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."""
[349]456 id = self.logger.fault(faultID, timestamp, what, score,
457 updatePrevious = updatePrevious,
458 updateID = updateID)
[31]459 self._gui.setRating(self.logger.getRating())
[349]460 return id
[30]461
462 def handleNoGo(self, faultID, timestamp, what, shortReason):
463 """Handle a No-Go fault."""
464 self.logger.noGo(faultID, timestamp, what)
[31]465 self._gui.setNoGo(shortReason)
[30]466
[349]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())
[354]490 if self._stage == const.STAGE_PUSHANDTAXI:
491 self.setStage(self.aircraft.state.timestamp,
492 const.STAGE_RTO)
[349]493 else:
494 SpeedChecker.logSpeedFault(self, self._rtoState,
495 stage = const.STAGE_PUSHANDTAXI,
496 updateID = self._rtoLogEntryID)
[354]497 if self._stage == const.STAGE_RTO:
498 self.setStage(self.aircraft.state.timestamp,
499 const.STAGE_PUSHANDTAXI)
[349]500
[9]501 def flareStarted(self, flareStart, flareStartFS):
502 """Called when the flare time has started."""
503 self._flareStart = flareStart
504 self._flareStartFS = flareStartFS
505
[170]506 def flareFinished(self, flareEnd, flareEndFS, tdRate):
[9]507 """Called when the flare time has ended.
[348]508
[9]509 Return a tuple of the following items:
510 - a boolean indicating if FS time is used
511 - the flare time
512 """
[170]513 self._tdRate = tdRate
[9]514 if self.flareTimeFromFS:
515 return (True, flareEndFS - self._flareStartFS)
516 else:
517 return (False, flareEnd - self._flareStart)
[8]518
519 def wait(self):
520 """Wait for the flight to end."""
521 with self._endCondition:
522 while self._stage!=const.STAGE_END:
[14]523 self._endCondition.wait(1)
[8]524
[134]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
[170]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
[241]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
[635]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
[241]547 def speedToKnots(self, speed):
548 """Convert the given speed expressed in the flight's speed unit into
[348]549 knots."""
[241]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
[635]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
[241]560 def getI18NSpeedUnit(self):
561 """Get the speed unit suffix for i18n message identifiers."""
562 return "_knots" if self.speedInKnots else "_kmph"
563
[274]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)
[348]570
[274]571 self.logger.message(aircraftState.timestamp, "Fuel: " + fuelStr)
572 self.logger.message(aircraftState.timestamp,
[348]573 "Total fuel: %.0f kg" % (aircraftState.totalFuel,))
[274]574
[304]575 def cruiseLevelChanged(self):
576 """Called when the cruise level hass changed."""
[383]577 if self.canLogCruiseAltitude(self._stage):
[304]578 message = "Cruise altitude modified to %d feet" % \
[383]579 (self._gui.loggableCruiseAltitude,)
[304]580 self.logger.message(self.aircraft.timestamp, message)
[383]581 return True
582 else:
583 return False
[304]584
[89]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)
[348]606
[89]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
[365]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
[8]641#---------------------------------------------------------------------------------------
Note: See TracBrowser for help on using the repository browser.