source: src/mlx/flight.py@ 1048:e5bfc691ad08

python3
Last change on this file since 1048:e5bfc691ad08 was 1034:4836f52b49cd, checked in by István Váradi <ivaradi@…>, 3 years ago

Updated the flight type handling (re #357)

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