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()
示例#2
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()
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
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 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)