source: src/mlx/flight.py@ 1033:330058d37574

python3
Last change on this file since 1033:330058d37574 was 1033:330058d37574, checked in by István Váradi <ivaradi@…>, 2 years ago

Updates for the new crew and passenger handling (re #357)

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