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()
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)