source: src/mlx/flight.py@ 1161:38b1f077d211

python3
Last change on this file since 1161:38b1f077d211 was 1161:38b1f077d211, checked in by István Váradi <ivaradi@…>, 7 months ago

Arriving earlier is never a warning or error to be explaned

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