source: src/mlx/flight.py@ 846:d11ea8564da8

Last change on this file since 846:d11ea8564da8 was 846:d11ea8564da8, checked in by István Váradi <ivaradi@…>, 6 years ago

The values are checked and the OK button is updated accordingly (re #307)

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