def __init__(self, parent, external, port):
		'''
		external is a host (possibly localhost) for external FGCom instance, or None for internal (child process)
		'''
		QWidget.__init__(self, parent)
		self.setupUi(self)
		client_address = some(external, 'localhost'), port
		self.settings = FgcomSettings(socket(AF_INET, SOCK_DGRAM), client_address)
		self.controller = Ticker(self.settings.send, parent=self)
		self.frequency_combo.addFrequencies([(frq, descr) for frq, descr, t in env.frequencies])
		self.frequency_combo.addFrequencies(frequencies_always_proposed)
		if external == None: # child process
			self.onOff_button.setToolTip('Internal FGCom instance using local port %d' % port)
			ad = world_navpoint_db.findClosest(env.radarPos(), types=[Navpoint.AD]).code if env.airport_data == None else settings.location_code
			self.instance = InternalFgcomInstance(port, ['--airport=%s' % ad], self)
			self.instance.started.connect(self.processHasStarted)
			self.instance.finished.connect(self.processHasStopped)
			self.onOff_button.toggled.connect(self.switchFGCom)
		else: # creating box for external instance
			self.instance = None
			self.onOff_button.setToolTip('External FGCom instance on %s:%d' % client_address)
			self.onOff_button.setChecked(True) # keep checked (tested for RDF)
			self.onOff_button.setEnabled(False)
			self.PTT_button.setEnabled(True)
			self.controller.start(fgcom_controller_ticker_interval)
		self.PTT_button.pressed.connect(lambda: self.PTT(True))
		self.PTT_button.released.connect(lambda: self.PTT(False))
		self.softVolume_tickBox.clicked.connect(self.setVolume)
		self.frequency_combo.frequencyChanged.connect(self.setFrequency)
		self.updateRDF()
		self.RDF_tickBox.toggled.connect(self.updateRDF)
		self.onOff_button.toggled.connect(self.updateRDF)
		signals.localSettingsChanged.connect(self.updateRDF)
Beispiel #2
0
 def __init__(self, gui):
     SessionManager.__init__(self, gui)
     self.session_type = SessionType.TEACHER
     self.gui = gui
     self.session_ticker = Ticker(self.tickSessionOnce, parent=gui)
     self.simulation_paused_at = None  # pause time if session is paused; None otherwise
     self.server = QTcpServer(gui)
     self.student_socket = None
     self.server.newConnection.connect(self.studentConnects)
     self.aircraft_list = []  # ControlledAircraft list
     self.current_local_weather = None  # initialised by the teaching console on sessionStarted
     self.noACK_traffic_count = 0
 def __init__(self, gui):
     QObject.__init__(self)
     self.ticker = Ticker(self.scan, parent=gui)
     self.last_sweep = now()  # to be updated with current time at each blip
     self.aircraft_list = []  # Aircraft list
     self.blips_invisible = {
     }  # str -> int; number of blips for which ACFT callsign has been invisible
     self.soft_links = []  # (Strip, Aircraft) pairs
     self.known_EMG_squawkers = set()  # str identifiers
     self.runway_occupation = {}  # int -> list of ACFT identifiers
     if env.airport_data != None:
         for i in range(env.airport_data.physicalRunwayCount()):
             self.runway_occupation[i] = []
class WwStripExchanger:
    def __init__(self, gui):
        self.updater = SxUpdater(gui)
        self.update_ticker = Ticker(self.updater.start, parent=gui)
        self.gui = gui
        self.running = False

    def start(self):
        self.update_ticker.start(SX_update_interval)
        self.running = True

    def stopAndWait(self):
        if self.isRunning(
        ):  # CHECK: do we need to wait for any running SXsender threads as well?
            self.update_ticker.stop()
            self.updater.wait()
            self.running = False
        self.updater.ATCs_on_last_run.clear()
        self.updater.current_contact_claims.clear()

    def isRunning(self):
        return self.running

    def connectedATCs(self):
        return self.updater.ATCs_on_last_run[:]

    def isConnected(self, atc_callsign):
        return any(atc.callsign == atc_callsign
                   for atc in self.updater.ATCs_on_last_run)

    def claimingContact(self, callsign):
        return self.updater.current_contact_claims.get(callsign, None)

    def handOver(self, strip, atc_id):
        '''
		returns cleanly if transfer is properly initiated (contact linked),
		otherwise: HandoverBlocked (handover aborted)
		'''
        acft = strip.linkedAircraft()
        if acft == None:
            raise HandoverBlocked(
                'Unlinked strips cannot be sent through the OpenRadar system.')
        else:
            SXsender(self.gui, strip, acft.identifier, handover=atc_id).start()
 def __init__(self, gui):
     SessionManager.__init__(self, gui)
     self.session_type = SessionType.SOLO
     self.session_ticker = Ticker(self.tickSessionOnce, parent=gui)
     self.weather_ticker = Ticker(self.setNewWeather, parent=gui)
     self.spawn_timer = QTimer(gui)
     self.spawn_timer.setSingleShot(True)
     self.voice_instruction_recogniser = None
     self.speech_synthesiser = None
     self.msg_is_from_session_manager = False  # set to True before sending to avoid chat msg being rejected
     if speech_recognition_available:
         try:
             self.voice_instruction_recogniser = InstructionRecogniser(gui)
         except RuntimeError as err:
             settings.solo_voice_instructions = False
             QMessageBox.critical(self.gui, 'Sphinx error', \
              'Error setting up the speech recogniser (check log): %s\nVoice instructions disabled.' % err)
     if speech_synthesis_available:
         try:
             self.speech_synthesiser = SpeechSynthesiser(gui)
         except Exception as err:
             settings.solo_voice_readback = False
             QMessageBox.critical(self.gui, 'Pyttsx error', \
              'Error setting up the speech synthesiser: %s\nPilot read-back disabled.' % err)
     self.controlled_traffic = []
     self.uncontrolled_traffic = []
     self.current_local_weather = None
     self.simulation_paused_at = None  # start time if session is paused; None otherwise
     self.spawn_timer.timeout.connect(
         lambda: self.spawnNewControlledAircraft(isSessionStart=False))
     self.playable_aircraft_types = settings.solo_aircraft_types[:]
     self.uncontrolled_aircraft_types = [
         t for t in known_aircraft_types() if cruise_speed(t) != None
     ]
     pop_all(self.playable_aircraft_types,
             lambda t: t not in known_aircraft_types())
     pop_all(self.playable_aircraft_types,
             lambda t: cruise_speed(t) == None)
Beispiel #6
0
class TeacherSessionManager(SessionManager):
    def __init__(self, gui):
        SessionManager.__init__(self, gui)
        self.session_type = SessionType.TEACHER
        self.gui = gui
        self.session_ticker = Ticker(self.tickSessionOnce, parent=gui)
        self.simulation_paused_at = None  # pause time if session is paused; None otherwise
        self.server = QTcpServer(gui)
        self.student_socket = None
        self.server.newConnection.connect(self.studentConnects)
        self.aircraft_list = []  # ControlledAircraft list
        self.current_local_weather = None  # initialised by the teaching console on sessionStarted
        self.noACK_traffic_count = 0

    def start(self):
        self.aircraft_list.clear()
        self.simulation_paused_at = None
        self.session_ticker.start_stopOnZero(teacher_ticker_interval)
        self.server.listen(port=settings.teaching_service_port)
        print('Teaching server ready on port %d' %
              settings.teaching_service_port)
        signals.specialTool.connect(self.createNewTraffic)
        signals.kbdPTT.connect(self.sendPTT)
        signals.sessionStarted.emit()

    def stop(self):
        if self.isRunning():
            self.session_ticker.stop()
            if self.studentConnected():
                self.shutdownStudentConnection()
            signals.specialTool.disconnect(self.createNewTraffic)
            signals.kbdPTT.disconnect(self.sendPTT)
            self.server.close()
            self.aircraft_list.clear()
            signals.sessionEnded.emit()

    def studentConnected(self):
        return self.student_socket != None

    def isRunning(self):
        return self.session_ticker.isActive(
        ) or self.simulation_paused_at != None

    def myCallsign(self):
        return teacher_callsign

    def getAircraft(self):
        return self.aircraft_list[:]

    def getWeather(self, station):
        return self.current_local_weather if station == settings.primary_METAR_station else None

    def postRadioChatMsg(self, msg):
        raise ValueError(
            'Public radio chat panel reserved for monitoring read-backs in teacher sessions. '
            'Use the ATC text chat system to communicate with the student.')

    def postAtcChatMsg(self, msg):
        if self.studentConnected():
            if msg.isPrivate():
                payload = '%s\n%s' % (msg.sender(), msg.txtOnly())
                self.student.sendMessage(
                    TeachingMsg(TeachingMsg.ATC_TEXT_CHAT, data=payload))
            else:
                raise ValueError(
                    'Only private messaging is enabled in tutoring sessions.')
        else:
            raise ValueError('No student connected.')

    ## CONNECTION MANAGEMENT

    def studentConnects(self):
        new_connection = self.server.nextPendingConnection()
        if new_connection:
            peer_address = new_connection.peerAddress().toString()
            print('Contacted by %s' % peer_address)
            if self.studentConnected():
                new_connection.disconnectFromHost()
                print('Client rejected. Student already connected.')
            else:
                self.student_socket = new_connection
                self.student_socket.disconnected.connect(
                    self.studentDisconnects)
                self.student_socket.disconnected.connect(
                    self.student_socket.deleteLater)
                self.student = TeachingSessionWire(self.student_socket)
                self.student.messageArrived.connect(self.receiveMsgFromStudent)
                env.ATCs.updateATC(student_callsign, None, None, None)
                self.noACK_traffic_count = 0
                self.sendWeather()
                self.sendATCs()
                self.tickSessionOnce()
                if self.simulation_paused_at != None:
                    self.student.sendMessage(
                        TeachingMsg(TeachingMsg.SIM_PAUSED))
                QMessageBox.information(
                    self.gui, 'Student connection',
                    'Student accepted from %s' % peer_address)
        else:
            print('WARNING: Connection attempt failed.')

    def studentDisconnects(self):
        self.shutdownStudentConnection()
        QMessageBox.information(self.gui, 'Student disconnection',
                                'Your student has disconnected.')

    def shutdownStudentConnection(self):
        self.student_socket.disconnected.disconnect(self.studentDisconnects)
        env.cpdlc.endAllDataLinks()
        env.ATCs.removeATC(student_callsign)
        self.student.messageArrived.disconnect(self.receiveMsgFromStudent)
        self.student_socket.disconnectFromHost()
        self.student_socket = None

    ## TEACHER MANAGEMENT

    def instructAircraftByCallsign(self, callsign, instr):
        try:
            acft = next(acft for acft in self.aircraft_list
                        if acft.identifier == callsign)
        except StopIteration:
            print('ERROR: Teacher aircraft not found: %s' % callsign)
            return
        try:
            acft.instruct([instr])
            acft.readBack([instr])
        except Instruction.Error as err:
            QMessageBox.critical(self.gui, 'Instruction error',
                                 speech_str2txt(str(err)))

    def createNewTraffic(self, spawn_coords, spawn_hdg):
        dialog = CreateTrafficDialog(spawn_coords, spawn_hdg, parent=self.gui)
        dialog.exec()
        if dialog.result() > 0:
            params = dialog.acftInitParams()
            params.XPDR_mode = new_traffic_XPDR_mode
            acft = ControlledAircraft(dialog.acftCallsign(), dialog.acftType(),
                                      params, None)
            acft.spawned = False
            acft.frozen = dialog.startFrozen()
            acft.tickOnce()
            self.aircraft_list.append(acft)
            if dialog.createStrip():
                strip = Strip()
                strip.writeDetail(FPL.CALLSIGN, acft.identifier)
                strip.writeDetail(FPL.ACFT_TYPE, acft.aircraft_type)
                strip.writeDetail(FPL.WTC, wake_turb_cat(acft.aircraft_type))
                strip.linkAircraft(acft)
                signals.receiveStrip.emit(strip)
            selection.selectAircraft(acft)

    def killAircraft(self, acft):
        if env.cpdlc.isConnected(acft.identifier):
            env.cpdlc.endDataLink(acft.identifier)
        pop_all(self.aircraft_list, lambda a: a is acft)
        if acft.spawned and self.studentConnected():
            self.student.sendMessage(
                TeachingMsg(TeachingMsg.ACFT_KILLED, data=acft.identifier))
        signals.aircraftKilled.emit(acft)

    def sendATCs(self):
        if self.studentConnected():
            msg = TeachingMsg(TeachingMsg.SX_LIST)
            for atc in env.ATCs.knownATCs(
                    lambda atc: atc.callsign != student_callsign):
                try:
                    frq = env.ATCs.getATC(
                        atc
                    ).frequency  # instance of CommFrequency class, or None
                except KeyError:
                    frq = None
                msg.appendData(atc if frq == None else '%s\t%s' % (atc, frq))
                msg.appendData('\n')
            self.student.sendMessage(msg)

    def setWeather(self, weather):  # assumed at primary location and newer
        self.current_local_weather = weather
        signals.newWeather.emit(settings.primary_METAR_station,
                                self.current_local_weather)
        self.sendWeather()

    def sendWeather(self):
        if self.studentConnected() and self.current_local_weather != None:
            self.student.sendMessage(
                TeachingMsg(TeachingMsg.WEATHER,
                            data=self.current_local_weather.METAR()))

    def sendPTT(self, ignore_button, on_off):
        if selection.acft != None and selection.acft.spawned:
            if on_off:
                env.rdf.receiveSignal(
                    selection.acft.identifier,
                    lambda acft=selection.acft: acft.coords())
            else:
                env.rdf.dieSignal(selection.acft.identifier)
            if self.studentConnected():
                str_data = '%d %s' % (on_off, selection.acft.identifier)
                self.student.sendMessage(
                    TeachingMsg(TeachingMsg.PTT, data=str_data))

    def requestCpdlcConnection(
            self, callsign):  # NOTE: student must confirm data link
        if self.studentConnected():
            self.student.sendMessage(
                TeachingMsg(TeachingMsg.CPDLC, data=('%s\n1' % callsign)))

    def transferCpdlcAuthority(
        self, acft_callsign, atc_callsign
    ):  # for teacher, ATC here is who to transfer *from* to student
        if self.studentConnected():
            self.student.sendMessage(TeachingMsg(TeachingMsg.CPDLC, \
              data=('%s\n%s%s' % (acft_callsign, CPDLC_transfer_cmd_prefix, atc_callsign)))) # NOTE: student must confirm data link

    def disconnectCpdlc(self, callsign):
        env.cpdlc.endDataLink(callsign)
        if self.studentConnected():
            self.student.sendMessage(
                TeachingMsg(TeachingMsg.CPDLC, data=('%s\n0' % callsign)))

    def sendCpdlcMsg(self, callsign, msg):
        link = env.cpdlc.currentDataLink(callsign)
        if link == None:
            return
        if msg.type() == CpdlcMessage.ACK and link.msgCount() > 0:
            last_msg = link.lastMsg()
            if not last_msg.isFromMe() and last_msg.type() == CpdlcMessage.INSTR \
              and yesNo_question(self.gui, 'ACK after received INSTR', last_msg.contents(), 'Execute instruction?'):
                try:
                    instr = Instruction.fromEncodedStr(last_msg.contents())
                    self.instructAircraftByCallsign(callsign, instr)
                except ValueError:  # raised by Instruction.fromEncodedStr
                    if not yesNo_question(self.gui, 'CPDLC comm error', \
                      'Unable to decode instruction.', 'Send ACK and perform manually?'):
                        return  # cancel sending any message
                except Instruction.Error as err:  # raised by TeacherSessionManager.instructAircraftByCallsign
                    if not yesNo_question(self.gui, 'CPDLC instruction error', \
                      'Unable to perform instruction: %s' % err, 'Send ACK anyway?'):
                        return  # cancel sending any message
                else:  # no problem executing instruction
                    selection.writeStripAssignment(instr)
        if self.studentConnected() and link != None:
            link.appendMessage(msg)
            self.student.sendMessage(
                TeachingMsg(
                    TeachingMsg.CPDLC,
                    data=('%s\n%s%s' %
                          (callsign, CPDLC_message_cmd_prefix, msg.text()))))

    def pauseSession(self):
        self.session_ticker.stop()
        self.simulation_paused_at = now()
        signals.sessionPaused.emit()
        if self.studentConnected():
            self.student.sendMessage(TeachingMsg(TeachingMsg.SIM_PAUSED))

    def resumeSession(self):
        pause_delay = now() - self.simulation_paused_at
        for acft in self.aircraft_list:
            acft.moveHistoryTimesForward(pause_delay)
        self.simulation_paused_at = None
        self.session_ticker.start_stopOnZero(teacher_ticker_interval)
        signals.sessionResumed.emit()
        if self.studentConnected():
            self.student.sendMessage(TeachingMsg(TeachingMsg.SIM_RESUMED))

    ## MESSAGES FROM STUDENT

    def receiveMsgFromStudent(self, msg):
        #DEBUG if msg.type != TeachingMsg.TRAFFIC:
        #DEBUG 	print('=== TEACHERS RECEIVES ===\n%s\n=== End ===' % msg.data)
        if msg.type == TeachingMsg.ATC_TEXT_CHAT:
            lines = msg.strData().split('\n')
            if len(lines) == 2:
                signals.incomingAtcTextMsg.emit(
                    ChatMessage(student_callsign,
                                lines[1],
                                recipient=lines[0],
                                private=True))
            else:
                print(
                    'ERROR: Invalid format in received ATC text chat from student.'
                )
        elif msg.type == TeachingMsg.STRIP_EXCHANGE:
            line_sep = msg.strData().split('\n', maxsplit=1)
            toATC = line_sep[0]
            strip = Strip.fromEncodedDetails(
                '' if len(line_sep) < 2 else line_sep[1])
            strip.writeDetail(received_from_detail, student_callsign)
            if toATC != teacher_callsign:
                strip.writeDetail(sent_to_detail, toATC)
            signals.receiveStrip.emit(strip)
        elif msg.type == TeachingMsg.WEATHER:  # requesting weather information
            if msg.strData() == settings.primary_METAR_station:
                self.sendWeather()
        elif msg.type == TeachingMsg.TRAFFIC:  # acknowledging a traffic message
            if self.noACK_traffic_count > 0:
                self.noACK_traffic_count -= 1
            else:
                print('ERROR: Student acknowledging unsent traffic?!')

        elif msg.type == TeachingMsg.CPDLC:
            # Msg format in 2 lines, first being ACFT callsign, second is either of the following:
            #  - connect/disconnect: "0" or "1"
            #  - data authority transfer: CPDLC_transfer_cmd_prefix + ATC callsign transferring to/from
            #  - other: CPDLC_message_cmd_prefix + encoded message string
            try:
                acft_callsign, line2 = msg.strData().split('\n', maxsplit=1)
                if line2 == '0':  # ACFT disconnected by student
                    if env.cpdlc.isConnected(acft_callsign):
                        env.cpdlc.endDataLink(acft_callsign)
                    else:  # student is rejecting a connection (unable CPDLC)
                        QMessageBox.warning(
                            self.gui, 'CPDLC connection failed',
                            'Student is not accepting CPDLC connections.')
                elif line2 == '1':  # student confirming ACFT log-on
                    env.cpdlc.beginDataLink(acft_callsign, student_callsign)
                elif line2.startswith(
                        CPDLC_transfer_cmd_prefix
                ):  # student transferring or confirming transfer
                    atc = line2[len(CPDLC_transfer_cmd_prefix):]
                    if env.cpdlc.isConnected(
                            acft_callsign
                    ):  # student initiating transfer to next ATC
                        env.cpdlc.endDataLink(acft_callsign, transferTo=atc)
                    else:  # student confirming proposed transfer
                        env.cpdlc.beginDataLink(acft_callsign,
                                                student_callsign,
                                                transferFrom=atc)
                elif line2.startswith(CPDLC_message_cmd_prefix
                                      ):  # student ATC sent a message
                    encoded_msg = line2[len(CPDLC_message_cmd_prefix):]
                    link = env.cpdlc.currentDataLink(acft_callsign)
                    if link == None:
                        print(
                            'Ignored CPDLC message sent to %s while not connected.'
                            % acft_callsign)
                    else:
                        link.appendMessage(
                            CpdlcMessage.fromText(False, encoded_msg))
                else:
                    print('Error decoding CPDLC command from student:', line2)
            except (IndexError, ValueError):
                print('Error decoding CPDLC message value from student')
        else:
            print('ERROR: Unhandled message type from student: %s' % msg.type)

    ## TICK

    def tickSessionOnce(self):
        pop_all(self.aircraft_list,
                lambda a: not env.pointInRadarRange(a.params.position))
        send_traffic_this_tick = self.studentConnected(
        ) and self.noACK_traffic_count < max_noACK_traffic
        for acft in self.aircraft_list:
            acft.tickOnce()
            fgms_packet = acft.fgmsLivePositionPacket()
            send_packet_to_views(fgms_packet)
            if send_traffic_this_tick and acft.spawned:
                self.student.sendMessage(
                    TeachingMsg(TeachingMsg.TRAFFIC, data=fgms_packet))
                self.noACK_traffic_count += 1

    ## STRIP EXCHANGE

    def stripDroppedOnATC(self, strip, sendto):
        if sendto == student_callsign:
            items = [teacher_callsign] + env.ATCs.knownATCs(
                lambda atc: atc.callsign != student_callsign)
            sender, ok = QInputDialog.getItem(self.gui,
                                              'Send strip to student',
                                              'Hand over strip from:',
                                              items,
                                              editable=False)
            if ok and self.studentConnected():
                msg_data = sender + '\n' + strip.encodeDetails(
                    handover_details)
                self.student.sendMessage(
                    TeachingMsg(TeachingMsg.STRIP_EXCHANGE, data=msg_data))
            else:
                raise HandoverBlocked('Cancelled by teacher.', silent=True)
        else:
            raise HandoverBlocked('Strips can only be sent to the student!')

    ## SNAPSHOTTING

    def situationSnapshot(self):
        return [acft.statusSnapshot() for acft in self.aircraft_list]

    def restoreSituation(self, situation_snapshot):
        while self.aircraft_list != []:
            self.killAircraft(self.aircraft_list[0])
        for acft_snapshot in situation_snapshot:
            self.aircraft_list.append(
                ControlledAircraft.fromStatusSnapshot(acft_snapshot))
        self.tickSessionOnce()
 def __init__(self, gui):
     self.updater = SxUpdater(gui)
     self.update_ticker = Ticker(self.updater.start, parent=gui)
     self.gui = gui
     self.running = False
class Radar(QObject):
    blip = pyqtSignal()
    newContact = pyqtSignal(Aircraft)
    lostContact = pyqtSignal(Aircraft)
    emergencySquawk = pyqtSignal(Aircraft)
    runwayIncursion = pyqtSignal(int, Aircraft)
    pathConflict = pyqtSignal()
    nearMiss = pyqtSignal()

    def __init__(self, gui):
        QObject.__init__(self)
        self.ticker = Ticker(self.scan, parent=gui)
        self.last_sweep = now()  # to be updated with current time at each blip
        self.aircraft_list = []  # Aircraft list
        self.blips_invisible = {
        }  # str -> int; number of blips for which ACFT callsign has been invisible
        self.soft_links = []  # (Strip, Aircraft) pairs
        self.known_EMG_squawkers = set()  # str identifiers
        self.runway_occupation = {}  # int -> list of ACFT identifiers
        if env.airport_data != None:
            for i in range(env.airport_data.physicalRunwayCount()):
                self.runway_occupation[i] = []

    def startSweeping(self):
        self.ticker.start_stopOnZero(settings.radar_sweep_interval)

    def stopSweeping(self):
        self.ticker.stop()

    def scan(self):
        visible_aircraft = {
            a.identifier: a
            for a in settings.session_manager.getAircraft()
            if a.isRadarVisible()
        }

        ## UPDATE AIRCRAFT LIST
        lost_contacts = []
        for got_acft in self.aircraft_list:
            try:
                ignore = visible_aircraft.pop(got_acft.identifier)
                got_acft.saveRadarSnapshot()
                self.blips_invisible[got_acft.identifier] = 0
            except KeyError:
                count = self.blips_invisible[got_acft.identifier]
                if count < settings.invisible_blips_before_contact_lost:
                    self.blips_invisible[got_acft.identifier] = count + 1
                else:
                    lost_contacts.append(got_acft.identifier)
        # Remove lost aircraft
        for acft in pop_all(self.aircraft_list,
                            lambda acft: acft.identifier in lost_contacts):
            strip = env.linkedStrip(acft)
            del self.blips_invisible[acft.identifier]
            self.known_EMG_squawkers.discard(acft.identifier)
            self.lostContact.emit(acft)
            if strip != None:
                signals.controlledContactLost.emit(strip, acft.coords())
        # Add newly visible aircraft
        for new_acft in visible_aircraft.values():
            new_acft.saveRadarSnapshot()
            self.aircraft_list.append(new_acft)
            self.blips_invisible[new_acft.identifier] = 0
            self.newContact.emit(new_acft)

        ## CHECK FOR NEW EMERGENCIY SQUAWKS
        for acft in self.aircraft_list:
            if acft.xpdrCode() in XPDR_emergency_codes:
                if acft.identifier not in self.known_EMG_squawkers:
                    self.known_EMG_squawkers.add(acft.identifier)
                    self.emergencySquawk.emit(acft)
            else:
                self.known_EMG_squawkers.discard(acft.identifier)

        ## CHECK FOR NEW/LOST RADAR IDENTIFICATIONS
        if settings.traffic_identification_assistant:
            found_S_links = []
            found_A_links = []
            for strip in env.strips.listStrips(
                    lambda s: s.linkedAircraft() == None):
                mode_S_found = False
                # Try mode S identification
                if strip.lookup(FPL.CALLSIGN) != None:
                    scs = strip.lookup(FPL.CALLSIGN).upper()
                    if env.strips.count(
                            lambda s: s.lookup(FPL.CALLSIGN) != None and s.
                            lookup(FPL.CALLSIGN).upper() == scs) == 1:
                        candidates = [
                            acft for acft in self.aircraft_list
                            if acft.xpdrCallsign() != None
                            and acft.xpdrCallsign().upper() == scs
                        ]
                        if len(candidates) == 1:
                            found_S_links.append((strip, candidates[0]))
                            mode_S_found = True
                # Try mode A identification
                if not mode_S_found:
                    ssq = strip.lookup(assigned_SQ_detail)
                    if ssq != None and env.strips.count(lambda s: \
                      s.lookup(assigned_SQ_detail) == ssq and s.linkedAircraft() == None) == 1: # only one non-linked strip with this SQ
                        candidates = [acft for acft in self.aircraft_list if not any(a is acft for s, a in found_S_links) \
                         and acft.xpdrCode() == ssq and env.linkedStrip(acft) == None]
                        if len(candidates) == 1:  # only one aircraft matching
                            found_A_links.append((strip, candidates[0]))
            for s, a in pop_all(
                    self.soft_links, lambda sl: not any(sl[0] is s and sl[
                        1] is a for s, a in found_S_links + found_A_links)):
                s.writeDetail(soft_link_detail, None)
            for s, a, m in [(s, a, True) for s, a in found_S_links
                            ] + [(s, a, False) for s, a in found_A_links]:
                if not any(sl[0] is s and sl[1] is a
                           for sl in self.soft_links):  # new found soft link
                    if strip.lookup(
                            received_from_detail
                    ) != None and settings.strip_autolink_on_ident and (
                            m or settings.strip_autolink_include_modeC):
                        s.linkAircraft(a)
                    else:  # strip not automatically linked; notify of a new identification
                        self.soft_links.append((s, a))
                        s.writeDetail(soft_link_detail, a)
                        signals.aircraftIdentification.emit(s, a, m)

        ## UPDATE POSITION/ROUTE WARNINGS
        conflicts = {
            acft.identifier: Conflict.NO_CONFLICT
            for acft in self.aircraft_list
        }
        traffic_for_route_checks = []
        for strip in env.strips.listStrips(
        ):  # check for position conflicts and build list of traffic to check for routes later
            acft = strip.linkedAircraft()
            if acft != None and acft.identifier in conflicts:  # controlled traffic with radar contact
                for other in self.aircraft_list:
                    if other is not acft and position_conflict_test(
                            acft, other
                    ) == Conflict.NEAR_MISS:  # positive separation loss detected
                        conflicts[acft.identifier] = conflicts[
                            other.identifier] = Conflict.NEAR_MISS
                if not bypass_route_conflict_check(strip):
                    traffic_for_route_checks.append(acft)
        if settings.route_conflict_warnings:  # check for route conflicts
            while traffic_for_route_checks != []:  # progressively emptying the list
                acft = traffic_for_route_checks.pop()
                for other in traffic_for_route_checks:
                    c = path_conflict_test(acft, other)
                    conflicts[acft.identifier] = max(
                        conflicts[acft.identifier], c)
                    conflicts[other.identifier] = max(
                        conflicts[other.identifier], c)
        # now update aircraft conflicts and emit signals if any are new
        new_near_miss = new_path_conflict = False
        for contact in self.aircraft_list:
            new_conflict = conflicts[contact.identifier]
            if new_conflict > contact.conflict:
                new_near_miss |= new_conflict == Conflict.NEAR_MISS
                new_path_conflict |= new_conflict in [
                    Conflict.DEPENDS_ON_ALT, Conflict.PATH_CONFLICT
                ]
            contact.conflict = new_conflict
        if new_path_conflict:
            self.pathConflict.emit()
        if new_near_miss:
            self.nearMiss.emit()

        ## UPDATE RUNWAY OCCUPATION
        for phrwy in self.runway_occupation:
            new_occ = []
            if settings.monitor_runway_occupation:
                rwy1, rwy2 = env.airport_data.physicalRunway(phrwy)
                width_metres = env.airport_data.physicalRunwayData(phrwy)[0]
                thr1 = rwy1.threshold().toRadarCoords()
                thr2 = rwy2.threshold().toRadarCoords()
                w = m2NM * width_metres
                for acft in self.aircraft_list:
                    if acft.considerOnGround():
                        if acft.coords().toRadarCoords().isBetween(
                                thr1, thr2, w / 2):  # ACFT is on RWY
                            new_occ.append(acft)
                            if not any(
                                    a is acft
                                    for a in self.runway_occupation[phrwy]
                            ):  # just entered the RWY: check if alarm must sound
                                try:
                                    boxed_link = env.strips.findStrip(
                                        lambda strip: strip.lookup(
                                            runway_box_detail) == phrwy
                                    ).linkedAircraft()
                                except StopIteration:  # no strip boxed on this runway
                                    if rwy1.inUse() or rwy2.inUse(
                                    ):  # entering a non-reserved but active RWY
                                        self.runwayIncursion.emit(phrwy, acft)
                                else:  # RWY is reserved
                                    if boxed_link == None and env.linkedStrip(
                                            acft
                                    ) == None or boxed_link is acft:
                                        # entering ACFT is the one cleared to enter, or can be
                                        if self.runway_occupation[
                                                phrwy] != []:  # some ACFT was/were already on RWY
                                            call_guilty = acft if boxed_link == None else self.runway_occupation[
                                                phrwy][0]
                                            self.runwayIncursion.emit(
                                                phrwy, call_guilty)
                                    else:  # entering ACFT is known to be different from the one cleared to enter
                                        self.runwayIncursion.emit(phrwy, acft)
            self.runway_occupation[phrwy] = new_occ

        # Finished aircraft stuff
        self.last_sweep = now()
        self.blip.emit()

    def runwayOccupation(self, phrwy):
        return self.runway_occupation[phrwy]

    def missedOnLastScan(self, acft_id):
        '''
		True if ACFT is known (i.e. not already lost) but was not picked up on last radar scan.
		'''
        try:
            return self.blips_invisible[acft_id] > 0
        except KeyError:
            return False

    def contacts(self):
        '''
		Returns a list of connected aircraft contacts
		'''
        return self.aircraft_list[:]

    def resetContacts(self):
        self.aircraft_list.clear()
        self.blips_invisible.clear()
        self.soft_links.clear()
        self.known_EMG_squawkers.clear()
        for phrwy in self.runway_occupation:
            self.runway_occupation[phrwy].clear()

    def silentlyForgetContact(self, killed):
        for popped in pop_all(
                self.aircraft_list,
                lambda acft: acft is killed):  # there should only be one
            del self.blips_invisible[killed.identifier]
            self.known_EMG_squawkers.discard(killed.identifier)
class SoloSessionManager(SessionManager):
    '''
	VIRTUAL!
	Subclass and define methods:
	- generateAircraftAndStrip(): return (ACFT, Strip) pair of possibly None values
	- handoverGuard(cs, atc): return str error msg if handover not OK
	'''
    def __init__(self, gui):
        SessionManager.__init__(self, gui)
        self.session_type = SessionType.SOLO
        self.session_ticker = Ticker(self.tickSessionOnce, parent=gui)
        self.weather_ticker = Ticker(self.setNewWeather, parent=gui)
        self.spawn_timer = QTimer(gui)
        self.spawn_timer.setSingleShot(True)
        self.voice_instruction_recogniser = None
        self.speech_synthesiser = None
        self.msg_is_from_session_manager = False  # set to True before sending to avoid chat msg being rejected
        if speech_recognition_available:
            try:
                self.voice_instruction_recogniser = InstructionRecogniser(gui)
            except RuntimeError as err:
                settings.solo_voice_instructions = False
                QMessageBox.critical(self.gui, 'Sphinx error', \
                 'Error setting up the speech recogniser (check log): %s\nVoice instructions disabled.' % err)
        if speech_synthesis_available:
            try:
                self.speech_synthesiser = SpeechSynthesiser(gui)
            except Exception as err:
                settings.solo_voice_readback = False
                QMessageBox.critical(self.gui, 'Pyttsx error', \
                 'Error setting up the speech synthesiser: %s\nPilot read-back disabled.' % err)
        self.controlled_traffic = []
        self.uncontrolled_traffic = []
        self.current_local_weather = None
        self.simulation_paused_at = None  # start time if session is paused; None otherwise
        self.spawn_timer.timeout.connect(
            lambda: self.spawnNewControlledAircraft(isSessionStart=False))
        self.playable_aircraft_types = settings.solo_aircraft_types[:]
        self.uncontrolled_aircraft_types = [
            t for t in known_aircraft_types() if cruise_speed(t) != None
        ]
        pop_all(self.playable_aircraft_types,
                lambda t: t not in known_aircraft_types())
        pop_all(self.playable_aircraft_types,
                lambda t: cruise_speed(t) == None)

    def start(self, traffic_count):
        if self.playable_aircraft_types == []:
            QMessageBox.critical(
                self.gui, 'Not enough ACFT types',
                'Cannot start simulation: not enough playable aircraft types.')
            env.ATCs.clear()
            return
        if self.voice_instruction_recogniser != None:
            self.voice_instruction_recogniser.startup()
            signals.kbdPTT.connect(self.voicePTT)
        if self.speech_synthesiser != None:
            self.speech_synthesiser.startup()
            signals.voiceMsg.connect(self.speech_synthesiser.radioMsg)
        self.controlled_traffic.clear()
        self.uncontrolled_traffic.clear()
        for i in range(traffic_count):
            self.spawnNewControlledAircraft(isSessionStart=True)
        self.adjustDistractorCount()
        self.simulation_paused_at = None
        self.setNewWeather()
        self.session_ticker.start_stopOnZero(solo_ticker_interval)
        self.startWeatherTicker()
        signals.voiceMsgRecognised.connect(self.handleVoiceInstrMessage)
        signals.soloSessionSettingsChanged.connect(self.startWeatherTicker)
        signals.soloSessionSettingsChanged.connect(self.adjustDistractorCount)
        signals.sessionStarted.emit()
        print('Solo simulation begins.')

    def stop(self):
        if self.isRunning():
            signals.voiceMsgRecognised.disconnect(self.handleVoiceInstrMessage)
            signals.soloSessionSettingsChanged.disconnect(
                self.startWeatherTicker)
            signals.soloSessionSettingsChanged.disconnect(
                self.adjustDistractorCount)
            if self.voice_instruction_recogniser != None:
                signals.kbdPTT.disconnect(self.voicePTT)
                self.voice_instruction_recogniser.shutdown()
                self.voice_instruction_recogniser.wait()
            if self.speech_synthesiser != None:
                signals.voiceMsg.disconnect(self.speech_synthesiser.radioMsg)
                self.speech_synthesiser.shutdown()
                self.speech_synthesiser.wait()
            self.spawn_timer.stop()
            self.weather_ticker.stop()
            self.simulation_paused_at = None
            self.session_ticker.stop()
            self.controlled_traffic.clear()
            self.uncontrolled_traffic.clear()
            signals.sessionEnded.emit()

    def isRunning(self):
        return self.session_ticker.isActive(
        ) or self.simulation_paused_at != None

    def myCallsign(self):
        return settings.location_code

    def getAircraft(self):
        return self.controlled_traffic + self.uncontrolled_traffic

    def pauseSession(self):
        if self.isRunning() and self.simulation_paused_at == None:
            self.simulation_paused_at = now()
            self.session_ticker.stop()
            signals.sessionPaused.emit()

    def resumeSession(self):
        if self.isRunning() and self.simulation_paused_at != None:
            pause_delay = now() - self.simulation_paused_at
            for acft in self.getAircraft():
                acft.moveHistoryTimesForward(pause_delay)
            self.session_ticker.start_stopOnZero(solo_ticker_interval)
            self.simulation_paused_at = None
            signals.sessionResumed.emit()

    ## WEATHER

    def getWeather(self, station):
        return self.current_local_weather if station == settings.primary_METAR_station else None

    def setNewWeather(self):
        wind_info = None if self.current_local_weather == None else self.current_local_weather.mainWind(
        )
        if wind_info == None:
            w1 = 10 * randint(1, 36)
            w2 = randint(5, 20)
            if env.airport_data != None and \
              not any(rwy.inUse() and abs(w1 - rwy.orientation().trueAngle()) <= 90 for rwy in env.airport_data.allRunways()):
                w1 += 180
        else:
            whdg, wspd, gusts, unit = wind_info
            w1 = whdg.trueAngle() + 10 * randint(-1, 1)
            w2 = bounded(5, wspd + randint(-4, 4), 20)
        windstr = '%03d%02dKT' % ((w1 - 1) % 360 + 1, w2)
        self.current_local_weather = mkWeather(settings.primary_METAR_station,
                                               wind=windstr)
        signals.newWeather.emit(settings.primary_METAR_station,
                                self.current_local_weather)

    def startWeatherTicker(self):
        self.weather_ticker.start_stopOnZero(
            settings.solo_weather_change_interval, immediate=False)

    ## COMMUNICATIONS

    def postRadioChatMsg(self, msg):
        if self.msg_is_from_session_manager:
            self.msg_is_from_session_manager = False
        else:
            raise ValueError('Text messages not supported in solo sessions.')

    def postAtcChatMsg(self, msg):
        raise ValueError('ATC chat not available in solo sessions.')

    ## TICKING AND SPAWNING

    def controlledAcftNeeded(self):
        return len(self.controlled_traffic) < settings.solo_max_aircraft_count

    def killAircraft(self, acft):
        if env.cpdlc.isConnected(acft.identifier):
            env.cpdlc.endDataLink(acft.identifier)
        if len(pop_all(self.controlled_traffic, lambda a: a is acft)) == 0:
            pop_all(self.uncontrolled_traffic, lambda a: a is acft)
        signals.aircraftKilled.emit(acft)

    def adjustDistractorCount(self):
        while len(
                self.uncontrolled_traffic
        ) > settings.solo_distracting_traffic_count:  # too many uncontrolled ACFT
            self.killAircraft(self.uncontrolled_traffic[0])
        for i in range(
                settings.solo_distracting_traffic_count -
                len(self.uncontrolled_traffic)):  # uncontrolled ACFT needed
            self.spawnNewUncontrolledAircraft()

    def spawnNewUncontrolledAircraft(self):
        rndpos = env.radarPos().moved(Heading(randint(1, 360), True),
                                      uniform(10, .8 * settings.radar_range))
        rndalt = StdPressureAlt(randint(1, 10) * 1000)
        if self.airbornePositionFullySeparated(rndpos, rndalt):
            acft_type = choice(self.uncontrolled_aircraft_types)
            params = SoloParams(Status(Status.AIRBORNE), rndpos, rndalt,
                                Heading(randint(1, 360), True),
                                cruise_speed(acft_type))
            params.XPDR_code = settings.uncontrolled_VFR_XPDR_code
            new_acft = self.mkAiAcft(acft_type, params, goal=None)
            if new_acft != None:
                self.uncontrolled_traffic.append(new_acft)

    def spawnNewControlledAircraft(self, isSessionStart=False):
        new_acft = None
        attempts = 0
        while new_acft == None and attempts < max_attempts_for_aircraft_spawn:
            new_acft, strip = self.generateAircraftAndStrip()
            attempts += 1
        if new_acft != None and self.controlledAcftNeeded(
        ) and self.simulation_paused_at == None:
            self.controlled_traffic.append(new_acft)
            if settings.controller_pilot_data_link and random(
            ) <= settings.solo_CPDLC_balance:
                env.cpdlc.beginDataLink(
                    new_acft.identifier,
                    self.myCallsign(),
                    transferFrom=strip.lookup(received_from_detail))
            if strip != None:
                if isSessionStart:
                    strip.linkAircraft(new_acft)
                    strip.writeDetail(received_from_detail, None)
                signals.receiveStrip.emit(strip)
            if not env.cpdlc.isConnected(new_acft.identifier):
                new_acft.makeInitialContact(
                    None if settings.location_radio_name ==
                    '' else settings.location_radio_name)

    def airbornePositionFullySeparated(self, pos, alt):
        try:
            horiz_near = [
                acft for acft in self.getAircraft()
                if acft.params.position.distanceTo(pos) <
                settings.horizontal_separation
            ]
            ignore = next(acft for acft in horiz_near if abs(
                acft.params.altitude.diff(alt)) < settings.vertical_separation)
            return False
        except StopIteration:  # No aircraft too close
            return True

    def groundPositionFullySeparated(self, pos, t):
        return all(
            ground_separated(acft, pos, t) for acft in self.getAircraft()
            if acft.isGroundStatus())

    def tickSessionOnce(self):
        if self.controlledAcftNeeded() and not self.spawn_timer.isActive():
            delay = randint(int(settings.solo_min_spawn_delay.total_seconds()),
                            int(settings.solo_max_spawn_delay.total_seconds()))
            self.spawn_timer.start(1000 * delay)
        self.adjustDistractorCount()
        pop_all(
            self.controlled_traffic, lambda a: a.released or not env.
            pointInRadarRange(a.params.position))
        pop_all(self.uncontrolled_traffic, lambda a: a.ticks_to_live == 0)
        for acft in self.getAircraft():
            acft.tickOnce()
            send_packet_to_views(acft.fgmsLivePositionPacket())

    def mkAiAcft(self, acft_type, params, goal):
        '''
		goal=None for UncontrolledAircraft; otherwise ControlledAircraft
		returns None if something prevented fresh ACFT creation, e.g. CallsignGenerationError.
		'''
        params.XPDR_mode = 'S' if acft_cat(acft_type) in ['jets', 'heavy'
                                                          ] else 'C'
        airlines = known_airline_codes()
        if env.airport_data != None:  # might be rendering in tower view, prefer ACFT with known liveries
            liveries_for_acft = FGFS_model_liveries.get(acft_type, {})
            if len(liveries_for_acft
                   ) > 0 and settings.solo_restrict_to_available_liveries:
                pop_all(airlines, lambda al: al not in liveries_for_acft)
        try:
            callsign = self.generateCallsign(acft_type, airlines)
            if goal == None:
                ms_to_live = 1000 * 60 * randint(10, 60 * 3)
                return UncontrolledAircraft(callsign, acft_type, params,
                                            ms_to_live // solo_ticker_interval)
            else:
                return ControlledAircraft(callsign, acft_type, params, goal)
        except CallsignGenerationError:
            return None

    ## DEALING WITH INSTRUCTIONS

    def sendCpdlcMsg(self, callsign, msg):
        link = env.cpdlc.currentDataLink(callsign)
        if link != None:
            link.appendMessage(msg)
            if msg.type(
            ) == CpdlcMessage.INSTR:  # other message types ignored (unimplemented in solo)
                try:
                    acft = next(
                        a for a in self.controlled_traffic if a.identifier ==
                        callsign)  # uncontrolled traffic is not in contact
                    acft.instruct([Instruction.fromEncodedStr(msg.contents())])
                    # FUTURE ingest before instruct to allow exception raised or (delayed?) WILCO msg before actually executing
                except StopIteration:  # ACFT not found or not connected
                    print('WARNING: Aircraft %s not found.' % callsign)
                except Instruction.Error as err:  # raised by ControlledAircraft.instruct
                    link.appendMessage(
                        CpdlcMessage(False,
                                     CpdlcMessage.REJECT,
                                     contents=str(err)))
                else:  # instruction sent and already accepted
                    link.appendMessage(CpdlcMessage(False, CpdlcMessage.ACK))

    def transferCpdlcAuthority(self, acft_callsign, atc_callsign):
        try:
            acft = next(a for a in self.controlled_traffic
                        if a.identifier == acft_callsign)
            guard = self.handoverGuard(acft, atc_callsign)
            if guard == None:
                env.cpdlc.endDataLink(acft_callsign, transferTo=atc_callsign)
                acft.released = True
            else:
                raise CpdlcAuthorityTransferFailed(acft_callsign, atc_callsign,
                                                   guard)
        except StopIteration:
            pass

    def disconnectCpdlc(self, callsign):
        env.cpdlc.endDataLink(callsign)

    def instrExpectedByVoice(self, itype):
        return settings.solo_voice_instructions \
         and itype in [Instruction.VECTOR_HDG, Instruction.VECTOR_ALT, Instruction.VECTOR_SPD, Instruction.HAND_OVER]

    def voicePTT(self, key, toggle):
        if self.voice_instruction_recogniser != None and settings.solo_voice_instructions and self.simulation_paused_at == None:
            if toggle:
                self.voice_instruction_recogniser.keyIn()
            else:
                self.voice_instruction_recogniser.keyOut()

    def rejectInstruction(self, msg):
        if settings.solo_erroneous_instruction_warning:
            QMessageBox.warning(self.gui, 'Erroneous/rejected instruction',
                                msg)

    def instructAircraftByCallsign(self, callsign, instr):
        if not self.instrExpectedByVoice(instr.type):
            self._instructSequence([instr], callsign)

    def _instructSequence(self, instructions, callsign):
        try:
            acft = next(a for a in self.controlled_traffic if a.identifier ==
                        callsign)  # uncontrolled traffic is not in contact
            self.msg_is_from_session_manager = True
            signals.chatInstructionSuggestion.emit(
                callsign, _instr_str(instructions, acft), True)
            try:
                acft.instruct(instructions)
                acft.readBack(instructions)
                if settings.solo_wilco_beeps:
                    signals.wilco.emit()
            except Instruction.Error as err:
                acft.say('Unable. %s' % err, True)
                self.rejectInstruction('%s: "%s"' %
                                       (callsign, speech_str2txt(str(err))))
        except StopIteration:
            self.msg_is_from_session_manager = True
            signals.chatInstructionSuggestion.emit(
                callsign, _instr_str(instructions, None), True)
            self.rejectInstruction('Nobody answering callsign %s' % callsign)

    def handleVoiceInstrMessage(self, radio_callsign_tokens, instructions):
        acft_matches = [
            acft for acft in self.getAircraft()
            if radio_callsign_match(radio_callsign_tokens, acft.identifier)
        ]
        if acft_matches == []:
            callsign_to_instruct = write_radio_callsign(radio_callsign_tokens)
        elif len(acft_matches) == 1:
            callsign_to_instruct = acft_matches[0].identifier
        else:
            acft_matches[0].say('Sorry, was this for me?', True)
            self.rejectInstruction('Used callsign matches several: %s' %
                                   ', '.join(acft.identifier
                                             for acft in acft_matches))
            return
        if len(acft_matches) == 1:
            for instr in instructions:
                if instr.type == Instruction.HAND_OVER:
                    guard = self.handoverGuard(acft_matches[0], instr.arg[0])
                    if guard != None:
                        acft_matches[0].say('Negative. Staying with you.',
                                            True)
                        self.rejectInstruction('Bad/untimely handover:\n%s' %
                                               guard)
                        return
        self._instructSequence(instructions, callsign_to_instruct)

    def stripDroppedOnATC(self, strip, atc):
        if not self.instrExpectedByVoice(Instruction.HAND_OVER):
            cs = strip.callsign(acft=True)
            try:
                acft = next(a for a in self.controlled_traffic
                            if a.identifier == cs)
                guard = self.handoverGuard(acft, atc)
                if guard == None:
                    transfer_selected_or_instruct(atc)
                else:
                    raise HandoverBlocked(guard)
            except StopIteration:
                return
Beispiel #10
0
	def __init__(self, launcher, parent=None):
		QMainWindow.__init__(self, parent)
		self.setupUi(self)
		self.central_workspace = WorkspaceWidget(self)
		self.setCentralWidget(self.central_workspace)
		self.installEventFilter(RadioKeyEventFilter(self))
		self.setAttribute(Qt.WA_DeleteOnClose)
		self.launcher = launcher
		settings.controlled_tower_viewer = FlightGearTowerViewer(self)
		settings.session_manager = SessionManager(self)
		self.setWindowTitle('%s - %s (%s)' % (self.windowTitle(), env.locationName(), settings.location_code))
		self.session_start_sound_lock_timer = QTimer(self)
		self.session_start_sound_lock_timer.setSingleShot(True)
		self.session_start_sound_lock_timer.timeout.connect(self.unlockSounds)
		
		## Restore saved dock layout
		try:
			with open(dock_layout_file, 'rb') as f: # Restore saved dock arrangement
				self.restoreState(f.read())
		except FileNotFoundError: # Fallback on default dock arrangement
			# left docks, top zone
			self.tabifyDockWidget(self.selection_info_dock, self.weather_dock)
			self.tabifyDockWidget(self.selection_info_dock, self.towerView_dock)
			self.tabifyDockWidget(self.selection_info_dock, self.navigator_dock)
			self.selection_info_dock.hide()
			# left docks, bottom zone
			self.tabifyDockWidget(self.instructions_dock, self.notepads_dock)
			self.tabifyDockWidget(self.instructions_dock, self.radio_dock)
			self.tabifyDockWidget(self.instructions_dock, self.FPLlist_dock)
			self.tabifyDockWidget(self.instructions_dock, self.CPDLC_dock)
			self.instructions_dock.hide()
			self.notepads_dock.hide()
			self.radio_dock.hide()
			self.CPDLC_dock.hide()
			# right docks
			self.rwyBoxes_dock.hide() # hiding this because bad position (user will drag 1st thing after raise)
			# bottom docks
			self.atcTextChat_dock.hide()
		
		## Permanent tool/status bar widgets
		self.selectionInfo_toolbar.addWidget(SelectionInfoToolbarWidget(self))
		self.METAR_statusBarLabel = QLabel()
		self.PTT_statusBarLabel = QLabel()
		self.RDF_statusBarLabel = QLabel()
		self.RDF_statusBarLabel.setToolTip('Current signal / last QDM')
		self.wind_statusBarLabel = QLabel()
		self.QNH_statusBarLabel = QLabel()
		self.QNH_statusBarLabel.setToolTip('hPa / inHg')
		self.clock_statusBarLabel = QLabel()
		self.alarmClock_statusBarButtons = [AlarmClockButton('1', self), AlarmClockButton('2', self)]
		self.statusbar.addWidget(self.METAR_statusBarLabel)
		self.statusbar.addPermanentWidget(self.PTT_statusBarLabel)
		self.statusbar.addPermanentWidget(self.RDF_statusBarLabel)
		self.statusbar.addPermanentWidget(self.wind_statusBarLabel)
		self.statusbar.addPermanentWidget(self.QNH_statusBarLabel)
		for b in self.alarmClock_statusBarButtons:
			self.statusbar.addPermanentWidget(b)
			b.alarm.connect(self.notification_pane.notifyAlarmClockTimedOut)
		self.statusbar.addPermanentWidget(self.clock_statusBarLabel)
		
		# Populate menus (toolbar visibility and airport viewpoints)
		toolbar_menu = QMenu()
		self.general_viewToolbar_action = self.general_toolbar.toggleViewAction()
		self.stripActions_viewToolbar_action = self.stripActions_toolbar.toggleViewAction()
		self.docks_viewToolbar_action = self.docks_toolbar.toggleViewAction()
		self.selectionInfo_viewToolbar_action = self.selectionInfo_toolbar.toggleViewAction()
		self.radarAssistance_viewToolbar_action = self.radarAssistance_toolbar.toggleViewAction()
		self.workspace_viewToolbar_action = self.workspace_toolbar.toggleViewAction()
		toolbar_menu.addAction(self.general_viewToolbar_action)
		toolbar_menu.addAction(self.stripActions_viewToolbar_action)
		toolbar_menu.addAction(self.docks_viewToolbar_action)
		toolbar_menu.addAction(self.selectionInfo_viewToolbar_action)
		toolbar_menu.addAction(self.radarAssistance_viewToolbar_action)
		toolbar_menu.addAction(self.workspace_viewToolbar_action)
		self.toolbars_view_menuAction.setMenu(toolbar_menu)
		
		if env.airport_data == None or len(env.airport_data.viewpoints) == 0:
			self.viewpointSelection_view_menuAction.setEnabled(False)
		else:
			viewPointSelection_menu = QMenu()
			viewPointSelection_actionGroup = QActionGroup(self)
			for vp_i, (vp_pos, vp_h, vp_name) in enumerate(env.airport_data.viewpoints):
				action = QAction('%s - %d ft ASFC' % (vp_name, vp_h + .5), self)
				action.setCheckable(True)
				action.triggered.connect(lambda ignore_checked, i=vp_i: self.selectIndicateViewpoint(i))
				viewPointSelection_actionGroup.addAction(action)
			actions = viewPointSelection_actionGroup.actions()
			viewPointSelection_menu.addActions(actions)
			self.viewpointSelection_view_menuAction.setMenu(viewPointSelection_menu)
			try:
				actions[settings.selected_viewpoint].setChecked(True)
			except IndexError:
				actions[0].setChecked(True)
		
		## Memory-persistent windows and dialogs
		self.solo_connect_dialog_AD = StartSoloDialog_AD(self)
		self.MP_connect_dialog = StartFlightGearMPdialog(self)
		self.start_student_session_dialog = StartStudentSessionDialog(self)
		self.recall_cheat_dialog = DiscardedStripsDialog(self, ShelfFilterModel(self, env.discarded_strips, False), 'Sent and deleted strips')
		self.shelf_dialog = DiscardedStripsDialog(self, ShelfFilterModel(self, env.discarded_strips, True), 'Strip shelf')
		self.environment_info_dialog = EnvironmentInfoDialog(self)
		self.about_dialog = AboutDialog(self)
		self.teaching_console = TeachingConsole(parent=self)
		self.unit_converter = UnitConversionWindow(parent=self)
		self.world_airport_navigator = WorldAirportNavigator(parent=self)
		self.quick_reference = QuickReference(parent=self)
		for w in self.teaching_console, self.unit_converter, self.world_airport_navigator, self.quick_reference:
			w.setWindowFlags(Qt.Window)
			w.installEventFilter(RadioKeyEventFilter(w))
		
		# Make a few actions always visible
		self.addAction(self.newStrip_action)
		self.addAction(self.newLinkedStrip_action)
		self.addAction(self.newFPL_action)
		self.addAction(self.newLinkedFPL_action)
		self.addAction(self.startTimer1_action)
		self.addAction(self.forceStartTimer1_action)
		self.addAction(self.startTimer2_action)
		self.addAction(self.forceStartTimer2_action)
		self.addAction(self.notificationSounds_options_action)
		self.addAction(self.quickReference_help_action)
		self.addAction(self.saveDockLayout_view_action)
		self.addAction(self.recallWindowState_view_action)
		
		# Populate icons
		self.newStrip_action.setIcon(QIcon(IconFile.action_newStrip))
		self.newLinkedStrip_action.setIcon(QIcon(IconFile.action_newLinkedStrip))
		self.newFPL_action.setIcon(QIcon(IconFile.action_newFPL))
		self.newLinkedFPL_action.setIcon(QIcon(IconFile.action_newLinkedFPL))
		self.teachingConsole_view_action.setIcon(QIcon(IconFile.panel_teaching))
		self.unitConversionTool_view_action.setIcon(QIcon(IconFile.panel_unitConv))
		self.worldAirportNavigator_view_action.setIcon(QIcon(IconFile.panel_airportList))
		self.environmentInfo_view_action.setIcon(QIcon(IconFile.panel_envInfo))
		self.generalSettings_options_action.setIcon(QIcon(IconFile.action_generalSettings))
		self.soloSessionSettings_system_action.setIcon(QIcon(IconFile.action_sessionSettings))
		self.runwaysInUse_options_action.setIcon(QIcon(IconFile.action_runwayUse))
		self.newLooseStripBay_view_action.setIcon(QIcon(IconFile.action_newLooseStripBay))
		self.newRadarScreen_view_action.setIcon(QIcon(IconFile.action_newRadarScreen))
		self.newStripRackPanel_view_action.setIcon(QIcon(IconFile.action_newRackPanel))
		self.popOutCurrentWindow_view_action.setIcon(QIcon(IconFile.action_popOutWindow))
		self.reclaimPoppedOutWindows_view_action.setIcon(QIcon(IconFile.action_reclaimWindows))
		self.primaryRadar_options_action.setIcon(QIcon(IconFile.option_primaryRadar))
		self.approachSpacingHints_options_action.setIcon(QIcon(IconFile.option_approachSpacingHints))
		self.runwayOccupationWarnings_options_action.setIcon(QIcon(IconFile.option_runwayOccupationMonitor))
		self.routeConflictWarnings_options_action.setIcon(QIcon(IconFile.option_routeConflictWarnings))
		self.trafficIdentification_options_action.setIcon(QIcon(IconFile.option_identificationAssistant))
		
		setDockAndActionIcon(IconFile.panel_ATCs, self.handovers_dockView_action, self.handover_dock)
		setDockAndActionIcon(IconFile.panel_atcChat, self.atcTextChat_dockView_action, self.atcTextChat_dock)
		setDockAndActionIcon(IconFile.panel_CPDLC, self.cpdlc_dockView_action, self.CPDLC_dock)
		setDockAndActionIcon(IconFile.panel_FPLs, self.FPLs_dockView_action, self.FPLlist_dock)
		setDockAndActionIcon(IconFile.panel_instructions, self.instructions_dockView_action, self.instructions_dock)
		setDockAndActionIcon(IconFile.panel_navigator, self.navpoints_dockView_action, self.navigator_dock)
		setDockAndActionIcon(IconFile.panel_notepads, self.notepads_dockView_action, self.notepads_dock)
		setDockAndActionIcon(IconFile.panel_notifications, self.notificationArea_dockView_action, self.notification_dock)
		setDockAndActionIcon(IconFile.panel_radios, self.fgcom_dockView_action, self.radio_dock)
		setDockAndActionIcon(IconFile.panel_runwayBox, self.runwayBoxes_dockView_action, self.rwyBoxes_dock)
		setDockAndActionIcon(IconFile.panel_selInfo, self.radarContactDetails_dockView_action, self.selection_info_dock)
		setDockAndActionIcon(IconFile.panel_racks, self.strips_dockView_action, self.strip_dock)
		setDockAndActionIcon(IconFile.panel_txtChat, self.radioTextChat_dockView_action, self.radioTextChat_dock)
		setDockAndActionIcon(IconFile.panel_twrView, self.towerView_dockView_action, self.towerView_dock)
		setDockAndActionIcon(IconFile.panel_weather, self.weather_dockView_action, self.weather_dock)
		
		# action TICKED STATES (set here before connections)
		self.windowedWorkspace_view_action.setChecked(settings.saved_workspace_windowed_view)
		self.verticalRwyBoxLayout_view_action.setChecked(settings.vertical_runway_box_layout)
		self.notificationSounds_options_action.setChecked(settings.notification_sounds_enabled)
		self.primaryRadar_options_action.setChecked(settings.primary_radar_active)
		self.routeConflictWarnings_options_action.setChecked(settings.route_conflict_warnings)
		self.trafficIdentification_options_action.setChecked(settings.traffic_identification_assistant)
		self.runwayOccupationWarnings_options_action.setChecked(settings.monitor_runway_occupation)
		self.approachSpacingHints_options_action.setChecked(settings.APP_spacing_hints)
		
		# action CONNECTIONS
		# non-menu actions
		self.newStrip_action.triggered.connect(lambda: new_strip_dialog(self, default_rack_name, linkToSelection=False))
		self.newLinkedStrip_action.triggered.connect(lambda: new_strip_dialog(self, default_rack_name, linkToSelection=True))
		self.newFPL_action.triggered.connect(lambda: self.FPLlist_pane.createLocalFPL(link=None))
		self.newLinkedFPL_action.triggered.connect(lambda: self.FPLlist_pane.createLocalFPL(link=selection.strip))
		self.startTimer1_action.triggered.connect(lambda: self.startTimer(0, False))
		self.forceStartTimer1_action.triggered.connect(lambda: self.startTimer(0, True))
		self.startTimer2_action.triggered.connect(lambda: self.startTimer(1, False))
		self.forceStartTimer2_action.triggered.connect(lambda: self.startTimer(1, True))
		# system menu
		self.soloSession_system_action.triggered.connect(lambda: self.startStopSession(self.start_solo))
		self.connectFlightGearMP_system_action.triggered.connect(lambda: self.startStopSession(self.start_FlightGearMP))
		self.teacherSession_system_action.triggered.connect(lambda: self.startStopSession(self.start_teaching))
		self.studentSession_system_action.triggered.connect(lambda: self.startStopSession(self.start_learning))
		self.reloadAdditionalViewers_system_action.triggered.connect(self.reloadAdditionalViewers)
		self.reloadBgImages_system_action.triggered.connect(self.reloadBackgroundImages)
		self.reloadColourConfig_system_action.triggered.connect(self.reloadColourConfig)
		self.reloadRoutePresets_system_action.triggered.connect(self.reloadRoutePresets)
		self.reloadEntryExitPoints_system_action.triggered.connect(self.reloadEntryExitPoints)
		self.announceFgSession_system_action.triggered.connect(self.announceFgSession)
		self.fgcomEchoTest_system_action.triggered.connect(self.radio_pane.performEchoTest)
		self.extractSectorFile_system_action.triggered.connect(self.extractSectorFile)
		self.repositionBgImages_system_action.triggered.connect(self.repositionRadarBgImages)
		self.measuringLogsCoordinates_system_action.toggled.connect(self.switchMeasuringCoordsLog)
		self.airportGateway_system_action.triggered.connect(lambda: self.goToURL(airport_gateway_URL))
		self.openStreetMap_system_action.triggered.connect(lambda: self.goToURL(mk_OSM_URL(env.radarPos())))
		self.soloSessionSettings_system_action.triggered.connect(self.openSoloSessionSettings)
		self.locationSettings_system_action.triggered.connect(self.openLocalSettings)
		self.systemSettings_system_action.triggered.connect(self.openSystemSettings)
		self.changeLocation_system_action.triggered.connect(self.changeLocation)
		self.quit_system_action.triggered.connect(self.close)
		# view menu
		self.saveDockLayout_view_action.triggered.connect(self.saveDockLayout)
		self.recallWindowState_view_action.triggered.connect(self.recallWindowState)
		self.handovers_dockView_action.triggered.connect(lambda: self.raiseDock(self.handover_dock))
		self.atcTextChat_dockView_action.triggered.connect(lambda: self.raiseDock(self.atcTextChat_dock))
		self.cpdlc_dockView_action.triggered.connect(lambda: self.raiseDock(self.CPDLC_dock))
		self.FPLs_dockView_action.triggered.connect(lambda: self.raiseDock(self.FPLlist_dock))
		self.instructions_dockView_action.triggered.connect(lambda: self.raiseDock(self.instructions_dock))
		self.navpoints_dockView_action.triggered.connect(lambda: self.raiseDock(self.navigator_dock))
		self.notepads_dockView_action.triggered.connect(lambda: self.raiseDock(self.notepads_dock))
		self.notificationArea_dockView_action.triggered.connect(lambda: self.raiseDock(self.notification_dock))
		self.fgcom_dockView_action.triggered.connect(lambda: self.raiseDock(self.radio_dock))
		self.runwayBoxes_dockView_action.triggered.connect(lambda: self.raiseDock(self.rwyBoxes_dock))
		self.radarContactDetails_dockView_action.triggered.connect(lambda: self.raiseDock(self.selection_info_dock))
		self.strips_dockView_action.triggered.connect(lambda: self.raiseDock(self.strip_dock))
		self.radioTextChat_dockView_action.triggered.connect(lambda: self.raiseDock(self.radioTextChat_dock))
		self.towerView_dockView_action.triggered.connect(lambda: self.raiseDock(self.towerView_dock))
		self.weather_dockView_action.triggered.connect(lambda: self.raiseDock(self.weather_dock))
		self.windowedWorkspace_view_action.toggled.connect(self.central_workspace.switchWindowedView)
		self.popOutCurrentWindow_view_action.triggered.connect(self.central_workspace.popOutCurrentWindow)
		self.reclaimPoppedOutWindows_view_action.triggered.connect(self.central_workspace.reclaimPoppedOutWidgets)
		self.newLooseStripBay_view_action.triggered.connect(lambda: self.central_workspace.addWorkspaceWidget(WorkspaceWidget.LOOSE_BAY))
		self.newRadarScreen_view_action.triggered.connect(lambda: self.central_workspace.addWorkspaceWidget(WorkspaceWidget.RADAR_SCREEN))
		self.newStripRackPanel_view_action.triggered.connect(lambda: self.central_workspace.addWorkspaceWidget(WorkspaceWidget.STRIP_PANEL))
		self.verticalRwyBoxLayout_view_action.toggled.connect(self.switchVerticalRwyBoxLayout)
		self.towerView_view_action.triggered.connect(self.toggleTowerWindow)
		self.addViewer_view_action.triggered.connect(self.addView)
		self.listViewers_view_action.triggered.connect(self.listAdditionalViews)
		self.activateAdditionalViewers_view_action.toggled.connect(self.activateAdditionalViews)
		self.removeViewer_view_action.triggered.connect(self.removeView)
		self.teachingConsole_view_action.triggered.connect(self.teaching_console.show)
		self.unitConversionTool_view_action.triggered.connect(self.unit_converter.show)
		self.worldAirportNavigator_view_action.triggered.connect(self.world_airport_navigator.show)
		self.environmentInfo_view_action.triggered.connect(self.environment_info_dialog.exec)
		# options menu
		self.runwaysInUse_options_action.triggered.connect(self.configureRunwayUse)
		self.notificationSounds_options_action.toggled.connect(self.switchNotificationSounds)
		self.primaryRadar_options_action.toggled.connect(self.switchPrimaryRadar)
		self.routeConflictWarnings_options_action.toggled.connect(self.switchConflictWarnings)
		self.trafficIdentification_options_action.toggled.connect(self.switchTrafficIdentification)
		self.runwayOccupationWarnings_options_action.toggled.connect(self.switchRwyOccupationIndications)
		self.approachSpacingHints_options_action.toggled.connect(self.switchApproachSpacingHints)
		self.generalSettings_options_action.triggered.connect(self.openGeneralSettings)
		# cheat menu
		self.pauseSimulation_cheat_action.toggled.connect(self.pauseSession)
		self.spawnAircraft_cheat_action.triggered.connect(self.spawnAircraft)
		self.killSelectedAircraft_cheat_action.triggered.connect(self.killSelectedAircraft)
		self.popUpMsgOnRejectedInstr_cheat_action.toggled.connect(self.setRejectedInstrPopUp)
		self.showRecognisedVoiceStrings_cheat_action.toggled.connect(self.setShowRecognisedVoiceStrings)
		self.ensureClearWeather_cheat_action.toggled.connect(self.ensureClearWeather)
		self.ensureDayLight_cheat_action.triggered.connect(self.towerView_pane.ensureDayLight)
		self.changeTowerHeight_cheat_action.triggered.connect(self.changeTowerHeight)
		self.recallDiscardedStrip_cheat_action.triggered.connect(self.recall_cheat_dialog.exec)
		self.radarCheatMode_cheat_action.toggled.connect(self.setRadarCheatMode)
		# help menu
		self.quickReference_help_action.triggered.connect(self.quick_reference.show)
		self.videoTutorial_help_action.triggered.connect(lambda: self.goToURL(video_tutorial_URL))
		self.FAQ_help_action.triggered.connect(lambda: self.goToURL(FAQ_URL))
		self.about_help_action.triggered.connect(self.about_dialog.exec)
		
		## More signal connections
		signals.openShelfRequest.connect(self.shelf_dialog.exec)
		signals.privateAtcChatRequest.connect(lambda: self.raiseDock(self.atcTextChat_dock))
		signals.stripRecall.connect(recover_strip)
		env.radar.blip.connect(env.strips.refreshViews)
		env.radar.lostContact.connect(self.aircraftHasDisappeared)
		signals.aircraftKilled.connect(self.aircraftHasDisappeared)
		env.strips.rwyBoxFreed.connect(lambda box, strip: env.airport_data.physicalRunway_restartWtcTimer(box, strip.lookup(FPL.WTC)))
		env.rdf.signalChanged.connect(self.updateRDF)
		signals.statusBarMsg.connect(lambda msg: self.statusbar.showMessage(msg, status_bar_message_timeout))
		signals.newWeather.connect(self.updateWeatherIfPrimary)
		signals.kbdPTT.connect(self.updatePTT)
		signals.sessionStarted.connect(self.sessionHasStarted)
		signals.sessionEnded.connect(self.sessionHasEnded)
		signals.towerViewProcessToggled.connect(self.towerView_view_action.setChecked)
		signals.towerViewProcessToggled.connect(self.towerView_cheat_menu.setEnabled)
		signals.stripInfoChanged.connect(env.strips.refreshViews)
		signals.fastClockTick.connect(self.updateClock)
		signals.fastClockTick.connect(env.cpdlc.updateAckStatuses)
		signals.slowClockTick.connect(strip_auto_print_check)
		signals.stripEditRequest.connect(lambda strip: edit_strip(self, strip))
		signals.selectionChanged.connect(self.updateStripFplActions)
		signals.receiveStrip.connect(receive_strip)
		signals.handoverFailure.connect(self.recoverFailedHandover)
		signals.sessionPaused.connect(env.radar.stopSweeping)
		signals.sessionResumed.connect(env.radar.startSweeping)
		signals.aircraftKilled.connect(env.radar.silentlyForgetContact)
		signals.rackVisibilityLost.connect(self.collectClosedRacks)
		signals.localSettingsChanged.connect(env.rdf.clearAllSignals)
		signals.localSettingsChanged.connect(self.updateRDF)
		
		## MISC GUI setup
		self.strip_pane.setViewRacks([default_rack_name]) # will be moved out if a rack panel's saved "visible_racks" claims it [*1]
		self.strip_pane.restoreState(settings.saved_strip_dock_state) # [*1]
		self.central_workspace.restoreWorkspaceWindows(settings.saved_workspace_windows)
		self.central_workspace.switchWindowedView(settings.saved_workspace_windowed_view) # keep this after restoring windows!
		
		self.subsecond_ticker = Ticker(signals.fastClockTick.emit, parent=self)
		self.subminute_ticker = Ticker(signals.slowClockTick.emit, parent=self)
		self.subsecond_ticker.start_stopOnZero(subsecond_tick_interval)
		self.subminute_ticker.start_stopOnZero(subminute_tick_interval)
		self.towerView_cheat_menu.setEnabled(False)
		self.solo_cheat_menu.setEnabled(False)
		self.updateClock()
		self.updateWeatherIfPrimary(settings.primary_METAR_station, None)
		self.updateStripFplActions()
		self.last_RDF_qdm = None
		self.updateRDF()
		self.updatePTT(0, False)
		# Disable some base airport stuff if doing CTR
		if env.airport_data == None:
			self.towerView_view_action.setEnabled(False)
			self.runwaysInUse_options_action.setEnabled(False)
			self.runwayOccupationWarnings_options_action.setEnabled(False)
		# Finish
		self.atcTextChat_pane.switchAtcChatFilter(None) # Show GUI on general chat room at start
		if speech_recognition_available:
			prepare_SR_language_files()
Beispiel #11
0
class MainWindow(QMainWindow, Ui_mainWindow):
	def __init__(self, launcher, parent=None):
		QMainWindow.__init__(self, parent)
		self.setupUi(self)
		self.central_workspace = WorkspaceWidget(self)
		self.setCentralWidget(self.central_workspace)
		self.installEventFilter(RadioKeyEventFilter(self))
		self.setAttribute(Qt.WA_DeleteOnClose)
		self.launcher = launcher
		settings.controlled_tower_viewer = FlightGearTowerViewer(self)
		settings.session_manager = SessionManager(self)
		self.setWindowTitle('%s - %s (%s)' % (self.windowTitle(), env.locationName(), settings.location_code))
		self.session_start_sound_lock_timer = QTimer(self)
		self.session_start_sound_lock_timer.setSingleShot(True)
		self.session_start_sound_lock_timer.timeout.connect(self.unlockSounds)
		
		## Restore saved dock layout
		try:
			with open(dock_layout_file, 'rb') as f: # Restore saved dock arrangement
				self.restoreState(f.read())
		except FileNotFoundError: # Fallback on default dock arrangement
			# left docks, top zone
			self.tabifyDockWidget(self.selection_info_dock, self.weather_dock)
			self.tabifyDockWidget(self.selection_info_dock, self.towerView_dock)
			self.tabifyDockWidget(self.selection_info_dock, self.navigator_dock)
			self.selection_info_dock.hide()
			# left docks, bottom zone
			self.tabifyDockWidget(self.instructions_dock, self.notepads_dock)
			self.tabifyDockWidget(self.instructions_dock, self.radio_dock)
			self.tabifyDockWidget(self.instructions_dock, self.FPLlist_dock)
			self.tabifyDockWidget(self.instructions_dock, self.CPDLC_dock)
			self.instructions_dock.hide()
			self.notepads_dock.hide()
			self.radio_dock.hide()
			self.CPDLC_dock.hide()
			# right docks
			self.rwyBoxes_dock.hide() # hiding this because bad position (user will drag 1st thing after raise)
			# bottom docks
			self.atcTextChat_dock.hide()
		
		## Permanent tool/status bar widgets
		self.selectionInfo_toolbar.addWidget(SelectionInfoToolbarWidget(self))
		self.METAR_statusBarLabel = QLabel()
		self.PTT_statusBarLabel = QLabel()
		self.RDF_statusBarLabel = QLabel()
		self.RDF_statusBarLabel.setToolTip('Current signal / last QDM')
		self.wind_statusBarLabel = QLabel()
		self.QNH_statusBarLabel = QLabel()
		self.QNH_statusBarLabel.setToolTip('hPa / inHg')
		self.clock_statusBarLabel = QLabel()
		self.alarmClock_statusBarButtons = [AlarmClockButton('1', self), AlarmClockButton('2', self)]
		self.statusbar.addWidget(self.METAR_statusBarLabel)
		self.statusbar.addPermanentWidget(self.PTT_statusBarLabel)
		self.statusbar.addPermanentWidget(self.RDF_statusBarLabel)
		self.statusbar.addPermanentWidget(self.wind_statusBarLabel)
		self.statusbar.addPermanentWidget(self.QNH_statusBarLabel)
		for b in self.alarmClock_statusBarButtons:
			self.statusbar.addPermanentWidget(b)
			b.alarm.connect(self.notification_pane.notifyAlarmClockTimedOut)
		self.statusbar.addPermanentWidget(self.clock_statusBarLabel)
		
		# Populate menus (toolbar visibility and airport viewpoints)
		toolbar_menu = QMenu()
		self.general_viewToolbar_action = self.general_toolbar.toggleViewAction()
		self.stripActions_viewToolbar_action = self.stripActions_toolbar.toggleViewAction()
		self.docks_viewToolbar_action = self.docks_toolbar.toggleViewAction()
		self.selectionInfo_viewToolbar_action = self.selectionInfo_toolbar.toggleViewAction()
		self.radarAssistance_viewToolbar_action = self.radarAssistance_toolbar.toggleViewAction()
		self.workspace_viewToolbar_action = self.workspace_toolbar.toggleViewAction()
		toolbar_menu.addAction(self.general_viewToolbar_action)
		toolbar_menu.addAction(self.stripActions_viewToolbar_action)
		toolbar_menu.addAction(self.docks_viewToolbar_action)
		toolbar_menu.addAction(self.selectionInfo_viewToolbar_action)
		toolbar_menu.addAction(self.radarAssistance_viewToolbar_action)
		toolbar_menu.addAction(self.workspace_viewToolbar_action)
		self.toolbars_view_menuAction.setMenu(toolbar_menu)
		
		if env.airport_data == None or len(env.airport_data.viewpoints) == 0:
			self.viewpointSelection_view_menuAction.setEnabled(False)
		else:
			viewPointSelection_menu = QMenu()
			viewPointSelection_actionGroup = QActionGroup(self)
			for vp_i, (vp_pos, vp_h, vp_name) in enumerate(env.airport_data.viewpoints):
				action = QAction('%s - %d ft ASFC' % (vp_name, vp_h + .5), self)
				action.setCheckable(True)
				action.triggered.connect(lambda ignore_checked, i=vp_i: self.selectIndicateViewpoint(i))
				viewPointSelection_actionGroup.addAction(action)
			actions = viewPointSelection_actionGroup.actions()
			viewPointSelection_menu.addActions(actions)
			self.viewpointSelection_view_menuAction.setMenu(viewPointSelection_menu)
			try:
				actions[settings.selected_viewpoint].setChecked(True)
			except IndexError:
				actions[0].setChecked(True)
		
		## Memory-persistent windows and dialogs
		self.solo_connect_dialog_AD = StartSoloDialog_AD(self)
		self.MP_connect_dialog = StartFlightGearMPdialog(self)
		self.start_student_session_dialog = StartStudentSessionDialog(self)
		self.recall_cheat_dialog = DiscardedStripsDialog(self, ShelfFilterModel(self, env.discarded_strips, False), 'Sent and deleted strips')
		self.shelf_dialog = DiscardedStripsDialog(self, ShelfFilterModel(self, env.discarded_strips, True), 'Strip shelf')
		self.environment_info_dialog = EnvironmentInfoDialog(self)
		self.about_dialog = AboutDialog(self)
		self.teaching_console = TeachingConsole(parent=self)
		self.unit_converter = UnitConversionWindow(parent=self)
		self.world_airport_navigator = WorldAirportNavigator(parent=self)
		self.quick_reference = QuickReference(parent=self)
		for w in self.teaching_console, self.unit_converter, self.world_airport_navigator, self.quick_reference:
			w.setWindowFlags(Qt.Window)
			w.installEventFilter(RadioKeyEventFilter(w))
		
		# Make a few actions always visible
		self.addAction(self.newStrip_action)
		self.addAction(self.newLinkedStrip_action)
		self.addAction(self.newFPL_action)
		self.addAction(self.newLinkedFPL_action)
		self.addAction(self.startTimer1_action)
		self.addAction(self.forceStartTimer1_action)
		self.addAction(self.startTimer2_action)
		self.addAction(self.forceStartTimer2_action)
		self.addAction(self.notificationSounds_options_action)
		self.addAction(self.quickReference_help_action)
		self.addAction(self.saveDockLayout_view_action)
		self.addAction(self.recallWindowState_view_action)
		
		# Populate icons
		self.newStrip_action.setIcon(QIcon(IconFile.action_newStrip))
		self.newLinkedStrip_action.setIcon(QIcon(IconFile.action_newLinkedStrip))
		self.newFPL_action.setIcon(QIcon(IconFile.action_newFPL))
		self.newLinkedFPL_action.setIcon(QIcon(IconFile.action_newLinkedFPL))
		self.teachingConsole_view_action.setIcon(QIcon(IconFile.panel_teaching))
		self.unitConversionTool_view_action.setIcon(QIcon(IconFile.panel_unitConv))
		self.worldAirportNavigator_view_action.setIcon(QIcon(IconFile.panel_airportList))
		self.environmentInfo_view_action.setIcon(QIcon(IconFile.panel_envInfo))
		self.generalSettings_options_action.setIcon(QIcon(IconFile.action_generalSettings))
		self.soloSessionSettings_system_action.setIcon(QIcon(IconFile.action_sessionSettings))
		self.runwaysInUse_options_action.setIcon(QIcon(IconFile.action_runwayUse))
		self.newLooseStripBay_view_action.setIcon(QIcon(IconFile.action_newLooseStripBay))
		self.newRadarScreen_view_action.setIcon(QIcon(IconFile.action_newRadarScreen))
		self.newStripRackPanel_view_action.setIcon(QIcon(IconFile.action_newRackPanel))
		self.popOutCurrentWindow_view_action.setIcon(QIcon(IconFile.action_popOutWindow))
		self.reclaimPoppedOutWindows_view_action.setIcon(QIcon(IconFile.action_reclaimWindows))
		self.primaryRadar_options_action.setIcon(QIcon(IconFile.option_primaryRadar))
		self.approachSpacingHints_options_action.setIcon(QIcon(IconFile.option_approachSpacingHints))
		self.runwayOccupationWarnings_options_action.setIcon(QIcon(IconFile.option_runwayOccupationMonitor))
		self.routeConflictWarnings_options_action.setIcon(QIcon(IconFile.option_routeConflictWarnings))
		self.trafficIdentification_options_action.setIcon(QIcon(IconFile.option_identificationAssistant))
		
		setDockAndActionIcon(IconFile.panel_ATCs, self.handovers_dockView_action, self.handover_dock)
		setDockAndActionIcon(IconFile.panel_atcChat, self.atcTextChat_dockView_action, self.atcTextChat_dock)
		setDockAndActionIcon(IconFile.panel_CPDLC, self.cpdlc_dockView_action, self.CPDLC_dock)
		setDockAndActionIcon(IconFile.panel_FPLs, self.FPLs_dockView_action, self.FPLlist_dock)
		setDockAndActionIcon(IconFile.panel_instructions, self.instructions_dockView_action, self.instructions_dock)
		setDockAndActionIcon(IconFile.panel_navigator, self.navpoints_dockView_action, self.navigator_dock)
		setDockAndActionIcon(IconFile.panel_notepads, self.notepads_dockView_action, self.notepads_dock)
		setDockAndActionIcon(IconFile.panel_notifications, self.notificationArea_dockView_action, self.notification_dock)
		setDockAndActionIcon(IconFile.panel_radios, self.fgcom_dockView_action, self.radio_dock)
		setDockAndActionIcon(IconFile.panel_runwayBox, self.runwayBoxes_dockView_action, self.rwyBoxes_dock)
		setDockAndActionIcon(IconFile.panel_selInfo, self.radarContactDetails_dockView_action, self.selection_info_dock)
		setDockAndActionIcon(IconFile.panel_racks, self.strips_dockView_action, self.strip_dock)
		setDockAndActionIcon(IconFile.panel_txtChat, self.radioTextChat_dockView_action, self.radioTextChat_dock)
		setDockAndActionIcon(IconFile.panel_twrView, self.towerView_dockView_action, self.towerView_dock)
		setDockAndActionIcon(IconFile.panel_weather, self.weather_dockView_action, self.weather_dock)
		
		# action TICKED STATES (set here before connections)
		self.windowedWorkspace_view_action.setChecked(settings.saved_workspace_windowed_view)
		self.verticalRwyBoxLayout_view_action.setChecked(settings.vertical_runway_box_layout)
		self.notificationSounds_options_action.setChecked(settings.notification_sounds_enabled)
		self.primaryRadar_options_action.setChecked(settings.primary_radar_active)
		self.routeConflictWarnings_options_action.setChecked(settings.route_conflict_warnings)
		self.trafficIdentification_options_action.setChecked(settings.traffic_identification_assistant)
		self.runwayOccupationWarnings_options_action.setChecked(settings.monitor_runway_occupation)
		self.approachSpacingHints_options_action.setChecked(settings.APP_spacing_hints)
		
		# action CONNECTIONS
		# non-menu actions
		self.newStrip_action.triggered.connect(lambda: new_strip_dialog(self, default_rack_name, linkToSelection=False))
		self.newLinkedStrip_action.triggered.connect(lambda: new_strip_dialog(self, default_rack_name, linkToSelection=True))
		self.newFPL_action.triggered.connect(lambda: self.FPLlist_pane.createLocalFPL(link=None))
		self.newLinkedFPL_action.triggered.connect(lambda: self.FPLlist_pane.createLocalFPL(link=selection.strip))
		self.startTimer1_action.triggered.connect(lambda: self.startTimer(0, False))
		self.forceStartTimer1_action.triggered.connect(lambda: self.startTimer(0, True))
		self.startTimer2_action.triggered.connect(lambda: self.startTimer(1, False))
		self.forceStartTimer2_action.triggered.connect(lambda: self.startTimer(1, True))
		# system menu
		self.soloSession_system_action.triggered.connect(lambda: self.startStopSession(self.start_solo))
		self.connectFlightGearMP_system_action.triggered.connect(lambda: self.startStopSession(self.start_FlightGearMP))
		self.teacherSession_system_action.triggered.connect(lambda: self.startStopSession(self.start_teaching))
		self.studentSession_system_action.triggered.connect(lambda: self.startStopSession(self.start_learning))
		self.reloadAdditionalViewers_system_action.triggered.connect(self.reloadAdditionalViewers)
		self.reloadBgImages_system_action.triggered.connect(self.reloadBackgroundImages)
		self.reloadColourConfig_system_action.triggered.connect(self.reloadColourConfig)
		self.reloadRoutePresets_system_action.triggered.connect(self.reloadRoutePresets)
		self.reloadEntryExitPoints_system_action.triggered.connect(self.reloadEntryExitPoints)
		self.announceFgSession_system_action.triggered.connect(self.announceFgSession)
		self.fgcomEchoTest_system_action.triggered.connect(self.radio_pane.performEchoTest)
		self.extractSectorFile_system_action.triggered.connect(self.extractSectorFile)
		self.repositionBgImages_system_action.triggered.connect(self.repositionRadarBgImages)
		self.measuringLogsCoordinates_system_action.toggled.connect(self.switchMeasuringCoordsLog)
		self.airportGateway_system_action.triggered.connect(lambda: self.goToURL(airport_gateway_URL))
		self.openStreetMap_system_action.triggered.connect(lambda: self.goToURL(mk_OSM_URL(env.radarPos())))
		self.soloSessionSettings_system_action.triggered.connect(self.openSoloSessionSettings)
		self.locationSettings_system_action.triggered.connect(self.openLocalSettings)
		self.systemSettings_system_action.triggered.connect(self.openSystemSettings)
		self.changeLocation_system_action.triggered.connect(self.changeLocation)
		self.quit_system_action.triggered.connect(self.close)
		# view menu
		self.saveDockLayout_view_action.triggered.connect(self.saveDockLayout)
		self.recallWindowState_view_action.triggered.connect(self.recallWindowState)
		self.handovers_dockView_action.triggered.connect(lambda: self.raiseDock(self.handover_dock))
		self.atcTextChat_dockView_action.triggered.connect(lambda: self.raiseDock(self.atcTextChat_dock))
		self.cpdlc_dockView_action.triggered.connect(lambda: self.raiseDock(self.CPDLC_dock))
		self.FPLs_dockView_action.triggered.connect(lambda: self.raiseDock(self.FPLlist_dock))
		self.instructions_dockView_action.triggered.connect(lambda: self.raiseDock(self.instructions_dock))
		self.navpoints_dockView_action.triggered.connect(lambda: self.raiseDock(self.navigator_dock))
		self.notepads_dockView_action.triggered.connect(lambda: self.raiseDock(self.notepads_dock))
		self.notificationArea_dockView_action.triggered.connect(lambda: self.raiseDock(self.notification_dock))
		self.fgcom_dockView_action.triggered.connect(lambda: self.raiseDock(self.radio_dock))
		self.runwayBoxes_dockView_action.triggered.connect(lambda: self.raiseDock(self.rwyBoxes_dock))
		self.radarContactDetails_dockView_action.triggered.connect(lambda: self.raiseDock(self.selection_info_dock))
		self.strips_dockView_action.triggered.connect(lambda: self.raiseDock(self.strip_dock))
		self.radioTextChat_dockView_action.triggered.connect(lambda: self.raiseDock(self.radioTextChat_dock))
		self.towerView_dockView_action.triggered.connect(lambda: self.raiseDock(self.towerView_dock))
		self.weather_dockView_action.triggered.connect(lambda: self.raiseDock(self.weather_dock))
		self.windowedWorkspace_view_action.toggled.connect(self.central_workspace.switchWindowedView)
		self.popOutCurrentWindow_view_action.triggered.connect(self.central_workspace.popOutCurrentWindow)
		self.reclaimPoppedOutWindows_view_action.triggered.connect(self.central_workspace.reclaimPoppedOutWidgets)
		self.newLooseStripBay_view_action.triggered.connect(lambda: self.central_workspace.addWorkspaceWidget(WorkspaceWidget.LOOSE_BAY))
		self.newRadarScreen_view_action.triggered.connect(lambda: self.central_workspace.addWorkspaceWidget(WorkspaceWidget.RADAR_SCREEN))
		self.newStripRackPanel_view_action.triggered.connect(lambda: self.central_workspace.addWorkspaceWidget(WorkspaceWidget.STRIP_PANEL))
		self.verticalRwyBoxLayout_view_action.toggled.connect(self.switchVerticalRwyBoxLayout)
		self.towerView_view_action.triggered.connect(self.toggleTowerWindow)
		self.addViewer_view_action.triggered.connect(self.addView)
		self.listViewers_view_action.triggered.connect(self.listAdditionalViews)
		self.activateAdditionalViewers_view_action.toggled.connect(self.activateAdditionalViews)
		self.removeViewer_view_action.triggered.connect(self.removeView)
		self.teachingConsole_view_action.triggered.connect(self.teaching_console.show)
		self.unitConversionTool_view_action.triggered.connect(self.unit_converter.show)
		self.worldAirportNavigator_view_action.triggered.connect(self.world_airport_navigator.show)
		self.environmentInfo_view_action.triggered.connect(self.environment_info_dialog.exec)
		# options menu
		self.runwaysInUse_options_action.triggered.connect(self.configureRunwayUse)
		self.notificationSounds_options_action.toggled.connect(self.switchNotificationSounds)
		self.primaryRadar_options_action.toggled.connect(self.switchPrimaryRadar)
		self.routeConflictWarnings_options_action.toggled.connect(self.switchConflictWarnings)
		self.trafficIdentification_options_action.toggled.connect(self.switchTrafficIdentification)
		self.runwayOccupationWarnings_options_action.toggled.connect(self.switchRwyOccupationIndications)
		self.approachSpacingHints_options_action.toggled.connect(self.switchApproachSpacingHints)
		self.generalSettings_options_action.triggered.connect(self.openGeneralSettings)
		# cheat menu
		self.pauseSimulation_cheat_action.toggled.connect(self.pauseSession)
		self.spawnAircraft_cheat_action.triggered.connect(self.spawnAircraft)
		self.killSelectedAircraft_cheat_action.triggered.connect(self.killSelectedAircraft)
		self.popUpMsgOnRejectedInstr_cheat_action.toggled.connect(self.setRejectedInstrPopUp)
		self.showRecognisedVoiceStrings_cheat_action.toggled.connect(self.setShowRecognisedVoiceStrings)
		self.ensureClearWeather_cheat_action.toggled.connect(self.ensureClearWeather)
		self.ensureDayLight_cheat_action.triggered.connect(self.towerView_pane.ensureDayLight)
		self.changeTowerHeight_cheat_action.triggered.connect(self.changeTowerHeight)
		self.recallDiscardedStrip_cheat_action.triggered.connect(self.recall_cheat_dialog.exec)
		self.radarCheatMode_cheat_action.toggled.connect(self.setRadarCheatMode)
		# help menu
		self.quickReference_help_action.triggered.connect(self.quick_reference.show)
		self.videoTutorial_help_action.triggered.connect(lambda: self.goToURL(video_tutorial_URL))
		self.FAQ_help_action.triggered.connect(lambda: self.goToURL(FAQ_URL))
		self.about_help_action.triggered.connect(self.about_dialog.exec)
		
		## More signal connections
		signals.openShelfRequest.connect(self.shelf_dialog.exec)
		signals.privateAtcChatRequest.connect(lambda: self.raiseDock(self.atcTextChat_dock))
		signals.stripRecall.connect(recover_strip)
		env.radar.blip.connect(env.strips.refreshViews)
		env.radar.lostContact.connect(self.aircraftHasDisappeared)
		signals.aircraftKilled.connect(self.aircraftHasDisappeared)
		env.strips.rwyBoxFreed.connect(lambda box, strip: env.airport_data.physicalRunway_restartWtcTimer(box, strip.lookup(FPL.WTC)))
		env.rdf.signalChanged.connect(self.updateRDF)
		signals.statusBarMsg.connect(lambda msg: self.statusbar.showMessage(msg, status_bar_message_timeout))
		signals.newWeather.connect(self.updateWeatherIfPrimary)
		signals.kbdPTT.connect(self.updatePTT)
		signals.sessionStarted.connect(self.sessionHasStarted)
		signals.sessionEnded.connect(self.sessionHasEnded)
		signals.towerViewProcessToggled.connect(self.towerView_view_action.setChecked)
		signals.towerViewProcessToggled.connect(self.towerView_cheat_menu.setEnabled)
		signals.stripInfoChanged.connect(env.strips.refreshViews)
		signals.fastClockTick.connect(self.updateClock)
		signals.fastClockTick.connect(env.cpdlc.updateAckStatuses)
		signals.slowClockTick.connect(strip_auto_print_check)
		signals.stripEditRequest.connect(lambda strip: edit_strip(self, strip))
		signals.selectionChanged.connect(self.updateStripFplActions)
		signals.receiveStrip.connect(receive_strip)
		signals.handoverFailure.connect(self.recoverFailedHandover)
		signals.sessionPaused.connect(env.radar.stopSweeping)
		signals.sessionResumed.connect(env.radar.startSweeping)
		signals.aircraftKilled.connect(env.radar.silentlyForgetContact)
		signals.rackVisibilityLost.connect(self.collectClosedRacks)
		signals.localSettingsChanged.connect(env.rdf.clearAllSignals)
		signals.localSettingsChanged.connect(self.updateRDF)
		
		## MISC GUI setup
		self.strip_pane.setViewRacks([default_rack_name]) # will be moved out if a rack panel's saved "visible_racks" claims it [*1]
		self.strip_pane.restoreState(settings.saved_strip_dock_state) # [*1]
		self.central_workspace.restoreWorkspaceWindows(settings.saved_workspace_windows)
		self.central_workspace.switchWindowedView(settings.saved_workspace_windowed_view) # keep this after restoring windows!
		
		self.subsecond_ticker = Ticker(signals.fastClockTick.emit, parent=self)
		self.subminute_ticker = Ticker(signals.slowClockTick.emit, parent=self)
		self.subsecond_ticker.start_stopOnZero(subsecond_tick_interval)
		self.subminute_ticker.start_stopOnZero(subminute_tick_interval)
		self.towerView_cheat_menu.setEnabled(False)
		self.solo_cheat_menu.setEnabled(False)
		self.updateClock()
		self.updateWeatherIfPrimary(settings.primary_METAR_station, None)
		self.updateStripFplActions()
		self.last_RDF_qdm = None
		self.updateRDF()
		self.updatePTT(0, False)
		# Disable some base airport stuff if doing CTR
		if env.airport_data == None:
			self.towerView_view_action.setEnabled(False)
			self.runwaysInUse_options_action.setEnabled(False)
			self.runwayOccupationWarnings_options_action.setEnabled(False)
		# Finish
		self.atcTextChat_pane.switchAtcChatFilter(None) # Show GUI on general chat room at start
		if speech_recognition_available:
			prepare_SR_language_files()
	
	def raiseDock(self, dock):
		dock.show()
		dock.raise_()
		dock.widget().setFocus()
		dock.setStyleSheet(dock_flash_stylesheet)
		QTimer.singleShot(dock_flash_time, lambda: dock.setStyleSheet(None))
	
	def startTimer(self, i, force_start):
		if force_start or not self.alarmClock_statusBarButtons[i].timerIsRunning():
			self.alarmClock_statusBarButtons[i].setTimer()
	
	def goToURL(self, url):
		if not QDesktopServices.openUrl(QUrl(url)):
			QMessageBox.critical(self, 'Error opening web browser', \
				'Could not invoke desktop web browser.\nGo to: %s' % url)

	def recoverFailedHandover(self, strip, msg):
		recover_strip(strip)
		QMessageBox.critical(self, 'Handover failed', '%s\nStrip has been recovered.' % msg)
	
	
	
	
	
	
	# ---------------------     GUI auto-update functions     ---------------------- #
	
	def updateClock(self):
		self.clock_statusBarLabel.setText('UTC ' + timestr(seconds=True))
	
	def unlockSounds(self):
		settings.session_start_sound_lock = False
	
	def updateWeatherIfPrimary(self, station, weather): # NOTE weather may be None here
		if station == settings.primary_METAR_station:
			# Update status bar info
			self.METAR_statusBarLabel.setText(None if weather == None else weather.METAR())
			self.wind_statusBarLabel.setText('Wind ' + ('---' if weather == None else weather.readWind()))
			qnh = None if weather == None else weather.QNH() # NOTE qnh may still be None
			self.QNH_statusBarLabel.setText('QNH ' + ('%d / %.2f' % (qnh, hPa2inHg * qnh) if qnh != None else '---'))
			# Update tower view
			if weather != None and not settings.TWR_view_clear_weather_cheat:
				settings.controlled_tower_viewer.setWeather(weather)
	
	def updatePTT(self, button, pressed):
		if settings.session_manager.isRunning():
			settings.transmitting_radio = pressed
			if settings.session_manager.session_type == SessionType.SOLO:
				if settings.solo_voice_instructions:
					self.PTT_statusBarLabel.setText('PTT' if pressed else 'VOICE')
				else:
					self.PTT_statusBarLabel.setText('MOUSE')
			elif pressed:
				self.PTT_statusBarLabel.setText('PTT')
			else:
				self.PTT_statusBarLabel.setText(' - - - ')
		else:
			settings.transmitting_radio = False
			self.PTT_statusBarLabel.setText('Off')
	
	def updateRDF(self):
		self.RDF_statusBarLabel.setVisible(settings.radio_direction_finding)
		if settings.radio_direction_finding:
			hdg = env.rdf.currentSignalRadial()
			if hdg == None:
				s1 = ' - - - '
			else:
				self.last_RDF_qdm = hdg.opposite()
				s1 = hdg.read()
			s2 = ' - - - ' if self.last_RDF_qdm == None else self.last_RDF_qdm.read()
			self.RDF_statusBarLabel.setText('RDF %s / %s' % (s1, s2))
	
	def updateSessionStartStopActions(self):
		running = settings.session_manager.isRunning()
		for gt, ma in {
					SessionType.SOLO: self.soloSession_system_action,
					SessionType.FLIGHTGEAR_MP: self.connectFlightGearMP_system_action,
					SessionType.STUDENT: self.studentSession_system_action,
					SessionType.TEACHER: self.teacherSession_system_action
				}.items():
			if gt == settings.session_manager.session_type:
				ma.setEnabled(True)
				ma.setChecked(running)
			else:
				ma.setEnabled(not running)
				ma.setChecked(False)
	
	def updateStripFplActions(self):
		self.newLinkedStrip_action.setEnabled(selection.strip == None and not selection.acft == selection.fpl == None)
		self.newLinkedFPL_action.setEnabled(selection.strip != None and selection.strip.linkedFPL() == None)
	
	def sessionHasStarted(self):
		self.session_start_sound_lock_timer.start(session_start_sound_lock_duration)
		self.updateSessionStartStopActions()
		self.solo_cheat_menu.setEnabled(settings.session_manager.session_type == SessionType.SOLO)
		self.updatePTT(0, False)
		env.radar.startSweeping()
		
	def sessionHasEnded(self):
		env.radar.stopSweeping()
		env.radar.resetContacts()
		env.strips.removeAllStrips()
		env.FPLs.clearFPLs()
		env.rdf.clearAllSignals()
		env.cpdlc.clearHistory()
		self.updateSessionStartStopActions()
		self.pauseSimulation_cheat_action.setChecked(False)
		self.solo_cheat_menu.setEnabled(False)
		self.updatePTT(0, False)
		self.last_RDF_qdm = None
		self.updateRDF()
		print('Session ended.')
	
	def aircraftHasDisappeared(self, acft):
		strip = env.linkedStrip(acft)
		if strip != None:
			strip.linkAircraft(None)
		if selection.acft is acft:
			if strip == None: # was not linked
				selection.deselect()
			else:
				selection.selectStrip(strip)
	
	def collectClosedRacks(self, racks):
		self.strip_pane.setViewRacks(self.strip_pane.getViewRacks() + racks)
	
	
	
	
	
	
	# ---------------------     Session start functions     ---------------------- #
	
	def start_solo(self):
		if env.airport_data == None: # start CTR solo simulation
			n, ok = QInputDialog.getInt(self, 'Solo CTR session', 'Initial traffic count:', value=2, min=0, max=99)
			if ok:
				settings.session_manager = SoloSessionManager_CTR(self)
				settings.session_manager.start(n)
		else: # start AD solo simulation
			self.solo_connect_dialog_AD.exec()
			if self.solo_connect_dialog_AD.result() > 0: # not rejected
				settings.session_manager = SoloSessionManager_AD(self)
				settings.session_manager.start(self.solo_connect_dialog_AD.chosenInitialTrafficCount())
	
	def start_FlightGearMP(self):
		self.MP_connect_dialog.exec()
		if self.MP_connect_dialog.result() > 0: # not rejected
			settings.session_manager = FlightGearMultiPlayerSessionManager(self, self.MP_connect_dialog.chosenCallsign())
			settings.session_manager.start()
	
	def start_teaching(self):
		port, ok = QInputDialog.getInt(self, 'Start a teaching session', 'Service port:', value=settings.teaching_service_port, max=99999)
		if ok:
			settings.teaching_service_port = port
			settings.session_manager = TeacherSessionManager(self)
			settings.session_manager.start()
	
	def start_learning(self):
		self.start_student_session_dialog.exec()
		if self.start_student_session_dialog.result() > 0: # not rejected
			settings.session_manager = StudentSessionManager(self)
			settings.session_manager.start()
	
	
	
	
	
	
	# ---------------------     GUI menu actions     ---------------------- #
	
	## SYSTEM MENU ##
	
	def startStopSession(self, start_func):
		if settings.session_manager.isRunning(): # Stop session
			selection.deselect()
			env.cpdlc.endAllDataLinks()
			settings.session_manager.stop()
		else: # Start session
			settings.session_start_sound_lock = True
			start_func()
		self.updateSessionStartStopActions()
		if not settings.session_manager.isRunning():
			settings.session_start_sound_lock = False
	
	def reloadAdditionalViewers(self):
		print('Reload: additional viewers')
		settings.loadAdditionalViews()
		QMessageBox.information(self, 'Done reloading', 'Additional viewers reloaded. Check for console error messages.')
	
	def reloadBackgroundImages(self):
		print('Reload: background images')
		settings.radar_background_images, settings.loose_strip_bay_backgrounds = read_bg_img(settings.location_code, env.navpoints)
		signals.backgroundImagesReloaded.emit()
		QMessageBox.information(self, 'Done reloading', 'Background images reloaded. Check for console error messages.')
	
	def reloadColourConfig(self):
		print('Reload: colour configuration')
		settings.loadColourSettings()
		signals.colourConfigReloaded.emit()
		QMessageBox.information(self, 'Done reloading', 'Colour configuration reloaded. Check for console error messages.')
	
	def reloadRoutePresets(self):
		print('Reload: route presets')
		settings.route_presets = read_route_presets()
		QMessageBox.information(self, 'Done reloading', 'Route presets reloaded. Check for console error messages.')
	
	def reloadEntryExitPoints(self):
		print('Reload: entry/exit points')
		world_routing_db.clearEntryExitPoints()
		import_entry_exit_data()
		QMessageBox.information(self, 'Done reloading', 'Entry/exit points reloaded. Check for console error messages.')
	
	def announceFgSession(self):
		if settings.lenny64_account_email == '':
			QMessageBox.critical(self, 'Lenny64 account details missing', \
				'This feature requires a Lenny64 dashboard. Please provide one in the FlightGear system set-up tab.')
		else:
			PostLennySessionDialog(self).exec()
	
	def switchMeasuringCoordsLog(self, toggle):
		settings.measuring_tool_logs_coordinates = toggle
	
	def extractSectorFile(self):
		txt, ignore = QFileDialog.getOpenFileName(self, caption='Select sector file to extract from')
		if txt != '':
			extract_sector(txt, env.radarPos(), settings.map_range)
			QMessageBox.information(self, 'Done extracting', \
				'Background drawings extracted.\nSee console for summary and files created in the output directory.')
	
	def repositionRadarBgImages(self):
		radar_panel = self.central_workspace.getCurrentRadarPanel()
		if radar_panel == None:
			QMessageBox.critical(self, 'Image positioning error', 'This requires an active radar panel in the central workspace.')
		else:
			radar_panel.positionVisibleBgImages()
	
	def openSoloSessionSettings(self):
		SoloSessionSettingsDialog(self).exec()
		self.updatePTT(0, False) # display "MOUSE/VOICE" as appropriate
	
	def openLocalSettings(self):
		dialog = LocalSettingsDialog(self)
		dialog.exec()
		if dialog.result() > 0 and settings.session_manager.isRunning():
			env.radar.startSweeping()
	
	def openSystemSettings(self):
		SystemSettingsDialog(self).exec()
	
	def changeLocation(self):
		if yesNo_question(self, 'Change location', 'This will close the current session.', 'Are you sure?'):
			self.launcher.show()
			self.close()
	
	
	## VIEW MENU ##
	
	def saveDockLayout(self): # STYLE catch file write error
		with open(dock_layout_file, 'wb') as f:
			f.write(self.saveState())
		QMessageBox.information(self, 'Save dock layout', 'Current dock layout saved.')
	
	def recallWindowState(self):
		try:
			with open(dock_layout_file, 'rb') as f:
				self.restoreState(f.read())
		except FileNotFoundError:
			QMessageBox.critical(self, 'Recall dock layout', 'No saved layout to recall.')
	
	def switchVerticalRwyBoxLayout(self, toggle):
		settings.vertical_runway_box_layout = toggle
		self.rwyBox_pane.setVerticalLayout(settings.vertical_runway_box_layout)
	
	def toggleTowerWindow(self):
		if self.towerView_view_action.isChecked():
			if env.airport_data != None and len(env.airport_data.viewpoints) == 0:
				QMessageBox.warning(self, 'No viewpoint', \
						'Airport data does not specify a viewpoint. ATC-pie is positioning one near a runway.\n'\
						'Update your source file if one should be available.')
			settings.controlled_tower_viewer.start()
		else:
			settings.controlled_tower_viewer.stop()
	
	def selectIndicateViewpoint(self, vp_index):
		if vp_index != settings.selected_viewpoint:
			settings.selected_viewpoint = vp_index
			settings.tower_height_cheat_offset = 0
			if settings.controlled_tower_viewer.running:
				self.towerView_pane.updateTowerPosition()
		signals.indicatePoint.emit(env.viewpoint()[0])
	
	def activateAdditionalViews(self, toggle):
		settings.additional_views_active = toggle
	
	def listAdditionalViews(self):
		if settings.additional_views == []:
			txt = 'No additional viewers registered.'
		else:
			lst = [' - %s on port %d' % host_port for host_port in settings.additional_views]
			txt = 'Additional viewers:\n%s' % '\n'.join(lst)
		QMessageBox.information(self, 'Additional viewers', txt)
	
	def addView(self):
		text, ok = QInputDialog.getText(self, 'Add an external viewer', 'Enter "host:port" to send traffic to:')
		if ok:
			split = text.rsplit(':', maxsplit=1)
			if len(split) == 2 and split[1].isdecimal():
				viewer_address = split[0], int(split[1])
				settings.additional_views.append(viewer_address)
				QMessageBox.information(self, 'Add an external viewer', 'Viewer added: %s on port %d.' % viewer_address)
			else:
				QMessageBox.critical(self, 'Add an external viewer', 'Bad "host:port" format.')
	
	def removeView(self):
		if settings.additional_views == []:
			QMessageBox.critical(self, 'Remove viewer', 'No additional viewers registered.')
		else:
			items = ['%d: %s on port %d' % (i, settings.additional_views[i][0], settings.additional_views[i][1]) \
								for i in range(len(settings.additional_views))]
			item, ok = QInputDialog.getItem(self, 'Remove viewer', 'Select viewer to remove:', items, editable=False)
			if ok:
				del settings.additional_views[int(item.split(':', maxsplit=1)[0])]
	
	
	## OPTIONS MENU ##
	
	def configureRunwayUse(self):
		RunwayUseDialog(self).exec()
	
	def switchNotificationSounds(self, toggle):
		settings.notification_sounds_enabled = toggle
	
	def switchPrimaryRadar(self, toggle):
		settings.primary_radar_active = toggle
		env.radar.scan()
	
	def switchConflictWarnings(self, toggle):
		settings.route_conflict_warnings = toggle
	
	def switchTrafficIdentification(self, toggle):
		settings.traffic_identification_assistant = toggle
		if not toggle:
			for strip in env.strips.listStrips():
				strip.writeDetail(soft_link_detail, None)
			signals.stripInfoChanged.emit()
	
	def switchRwyOccupationIndications(self, toggle):
		settings.monitor_runway_occupation = toggle
	
	def switchApproachSpacingHints(self, toggle):
		settings.APP_spacing_hints = toggle
		signals.stripInfoChanged.emit()
	
	def openGeneralSettings(self):
		GeneralSettingsDialog(self).exec()
	
	
	## CHEAT MENU ##
	
	def pauseSession(self, toggle):
		if toggle:
			settings.session_manager.pauseSession()
		else:
			settings.session_manager.resumeSession()
	
	def spawnAircraft(self):
		n, ok = QInputDialog.getInt(self, 'Spawn new aircraft', 'Try to spawn:', value=1, min=1, max=99)
		if ok:
			for i in range(n): # WARNING: session should be running
				settings.session_manager.spawnNewControlledAircraft()
	
	def killSelectedAircraft(self):
		selected = selection.acft
		if selected == None:
			QMessageBox.critical(self, 'Cheat error', 'No aircraft selected.')
		else:
			selection.deselect()
			settings.session_manager.killAircraft(selected) # WARNING: killAircraft method must exist
			env.radar.scan()
	
	def setRejectedInstrPopUp(self, toggle):
		settings.solo_erroneous_instruction_warning = toggle
	
	def ensureClearWeather(self, toggle):
		settings.TWR_view_clear_weather_cheat = toggle
		if toggle:
			weather = mkWeather(settings.location_code) # clear weather with location code as station
			settings.controlled_tower_viewer.setWeather(weather)
		else:
			weather = env.primaryWeather()
			if weather != None:
				settings.controlled_tower_viewer.setWeather(weather)
	
	def setShowRecognisedVoiceStrings(self, toggle):
		settings.show_recognised_voice_strings = toggle
	
	def changeTowerHeight(self):
		hoff, ok = QInputDialog.getInt(self, 'Cheat tower height', \
			'Offset in feet:', value=settings.tower_height_cheat_offset, min=0, max=1500, step=10)
		if ok:
			settings.tower_height_cheat_offset = hoff
			self.towerView_pane.updateTowerPosition()
	
	def setRadarCheatMode(self, toggle):
		settings.radar_cheat = toggle
		env.radar.scan()
	
	
	
	
	
	
	# -----------------     Internal GUI events      ------------------ #
	
	def closeEvent(self, event):
		if settings.session_manager.isRunning():
			settings.session_manager.stop()
		if settings.controlled_tower_viewer.running:
			settings.controlled_tower_viewer.stop(wait=True)
		if speech_recognition_available:
			cleanup_SR_language_files()
		print('Closing main window.')
		settings.saved_strip_racks = env.strips.rackNames()
		settings.saved_strip_dock_state = self.strip_pane.stateSave()
		settings.saved_workspace_windowed_view = self.central_workspace.windowedView()
		settings.saved_workspace_windows = self.central_workspace.workspaceWindowsStateSave()
		signals.mainWindowClosing.emit()
		signals.disconnect()
		settings.saveGeneralAndSystemSettings()
		settings.saveLocalSettings(env.airport_data)
		settings.savePresetChatMessages()
		env.resetEnv()
		settings.resetSession()
		EarthCoords.clearRadarPos()
		QMainWindow.closeEvent(self, event)
class RadioBox(QWidget, Ui_radioBox):
	def __init__(self, parent, external, port):
		'''
		external is a host (possibly localhost) for external FGCom instance, or None for internal (child process)
		'''
		QWidget.__init__(self, parent)
		self.setupUi(self)
		client_address = some(external, 'localhost'), port
		self.settings = FgcomSettings(socket(AF_INET, SOCK_DGRAM), client_address)
		self.controller = Ticker(self.settings.send, parent=self)
		self.frequency_combo.addFrequencies([(frq, descr) for frq, descr, t in env.frequencies])
		self.frequency_combo.addFrequencies(frequencies_always_proposed)
		if external == None: # child process
			self.onOff_button.setToolTip('Internal FGCom instance using local port %d' % port)
			ad = world_navpoint_db.findClosest(env.radarPos(), types=[Navpoint.AD]).code if env.airport_data == None else settings.location_code
			self.instance = InternalFgcomInstance(port, ['--airport=%s' % ad], self)
			self.instance.started.connect(self.processHasStarted)
			self.instance.finished.connect(self.processHasStopped)
			self.onOff_button.toggled.connect(self.switchFGCom)
		else: # creating box for external instance
			self.instance = None
			self.onOff_button.setToolTip('External FGCom instance on %s:%d' % client_address)
			self.onOff_button.setChecked(True) # keep checked (tested for RDF)
			self.onOff_button.setEnabled(False)
			self.PTT_button.setEnabled(True)
			self.controller.start(fgcom_controller_ticker_interval)
		self.PTT_button.pressed.connect(lambda: self.PTT(True))
		self.PTT_button.released.connect(lambda: self.PTT(False))
		self.softVolume_tickBox.clicked.connect(self.setVolume)
		self.frequency_combo.frequencyChanged.connect(self.setFrequency)
		self.updateRDF()
		self.RDF_tickBox.toggled.connect(self.updateRDF)
		self.onOff_button.toggled.connect(self.updateRDF)
		signals.localSettingsChanged.connect(self.updateRDF)
	
	def isInternal(self):
		return self.instance != None
	
	def clientAddress(self):
		return self.settings.address
	
	def getReadyForRemoval(self):
		if self.isInternal():
			self.switchFGCom(False)
			self.instance.waitForFinished(1000)
		else:
			self.controller.stop()
		try:
			del settings.MP_RDF_frequencies[self.settings.address[1]]
		except KeyError:
			pass
	
	def setVolume(self):
		if settings.FGCom_radios_muted:
			self.settings.vol = 0
		elif self.softVolume_tickBox.isChecked():
			self.settings.vol = soft_sound_level
		else:
			self.settings.vol = loud_sound_level
		self.settings.send()
		
	def setFrequency(self, frq):
		self.PTT(False)
		self.settings.frq = frq
		self.updateRDF()
		self.settings.send()
	
	def PTT(self, toggle):
		self.settings.ptt = toggle
		self.PTT_button.setChecked(toggle and self.PTT_button.isEnabled())
		# NOTE: line below is unreliable on mouse+kbd mix, but not a serious problem.
		settings.transmitting_radio = toggle # accounts for direct mouse PTT press
		self.settings.send()
	
	def updateRDF(self):
		frq_select_available = settings.radio_direction_finding and settings.session_manager.session_type == SessionType.FLIGHTGEAR_MP
		self.RDF_tickBox.setEnabled(frq_select_available)
		box_key = self.settings.address[1]
		if frq_select_available and self.RDF_tickBox.isChecked():
			settings.MP_RDF_frequencies[box_key] = self.settings.frq
		else:
			try:
				del settings.MP_RDF_frequencies[box_key]
			except KeyError:
				pass
	
	
	## Controlling FGCom process
	
	def switchFGCom(self, on_off):
		if self.isInternal():
			self.removeBox_button.setEnabled(not on_off)
		if on_off: # Start FGCom
			self.instance.start()
		else: # Stop FGCom
			self.PTT(False)
			self.instance.kill()
	
	def processHasStarted(self):
		self.PTT_button.setEnabled(True)
		self.controller.start(fgcom_controller_ticker_interval)
	
	def processHasStopped(self, exit_code, status):
		self.controller.stop()
		self.PTT_button.setEnabled(False)
		self.PTT_button.setChecked(False)
		self.onOff_button.setChecked(False)