예제 #1
0
    def runwayTurnOffs(self, rwy, maxangle=90, minroll=0):
        '''
		Returns a list tuple (L1, L2, L3, L4) containing turn-offs after a landing roll down the given RWY, in preferred order:
		- L1 is the list of preferred turn-offs, i.e. respecting maximum turn-off angle and ending off all runways
		- L2 is the list of sharper turn-offs down the runway and ending on taxiways
		- L3 is the list of turn-offs requiring a backtrack
		- L4 is the list of turn-offs ending on a runway
		In all cases a turn-off is a tuple (n1, n2, d, a) where:
		- n1 is the turn-off node on the runway
		- n2 is the arrival node where the runway is cleared
		- d is the distance from threshold to n1
		- a is the turn angle, between runway and (n1, n2) heading
		All lists are sorted by distance from current point.
		'''
        res = []
        for rwy_node in self.nodes(lambda n: self.nodeIsOnRunway(n, rwy.name)):
            for n in self.neighbours(rwy_node):
                if not self.nodeIsOnRunway(n, rwy.name):
                    rwy_point = self.nodePosition(rwy_node)
                    thr_dist = rwy.threshold().distanceTo(rwy_point)
                    turn_angle = rwy_point.headingTo(
                        self.nodePosition(n)).diff(rwy.orientation())
                    res.append((rwy_node, n, thr_dist, turn_angle))
        res.sort(key=(lambda t: t[2]))  # sort by distance to THR
        res_worst = pop_all(res, lambda t: len(self.connectedRunways(t[1])) > 0
                            )  # turn off on a RWY
        res_worse = [(n1, n2, d, (a + 180) % 360)
                     for n1, n2, d, a in pop_all(res, lambda t: t[2] < minroll)
                     ]  # need backtrack
        res_worse.reverse()
        res_bad = pop_all(
            res,
            lambda t: abs(t[3]) > max_rwy_turn_off_angle)  # sharp turn-off
        return res, res_bad, res_worse, res_worst
예제 #2
0
 def new_arrival_GND(self):
     acft_type = choice(self.parkable_aircraft_types)
     rwy = rnd_rwy([
         rwy
         for rwy in env.airport_data.allRunways() if rwy.use_for_arrivals
     ], lambda rwy: rwy.acceptsAcftType(acft_type))
     if rwy == None:
         return None
     turn_off_lists = l1, l2, l3, l4 = env.airport_data.ground_net.runwayTurnOffs(
         rwy, minroll=(rwy.length(dthr=True) * 2 / 3))
     for lst in turn_off_lists:
         pop_all(
             lst, lambda t: not self.groundPositionFullySeparated(
                 env.airport_data.ground_net.nodePosition(t[1]), acft_type))
     if all(lst == [] for lst in turn_off_lists):
         return None
     else:
         turn_off_choice = choice(l1) if l1 != [] else (l2 + l3)[0]
     pos = env.airport_data.ground_net.nodePosition(turn_off_choice[1])
     hdg = rwy.orientation() + turn_off_choice[3]
     params = SoloParams(Status(Status.TAXIING), pos,
                         env.groundStdPressureAlt(pos), hdg, Speed(0))
     params.XPDR_code = env.nextSquawkCodeAssignment(XPDR_range_IFR_ARR)
     pk_request = choice(
         env.airport_data.ground_net.parkingPositions(acftType=acft_type))
     return self.mkAiAcft(acft_type, params, pk_request)
예제 #3
0
 def readRunwaysInUse(self):
     if self.airport_data == None:
         return 'N/A'
     dep = [
         rwy.name for rwy in self.airport_data.allRunways()
         if rwy.use_for_departures
     ]
     arr = [
         rwy.name for rwy in self.airport_data.allRunways()
         if rwy.use_for_arrivals
     ]
     if dep + arr == []:
         return 'N/A'
     both = pop_all(dep, lambda rwy: rwy in arr)
     pop_all(arr, lambda rwy: rwy in both)
     res = '' if both == [] else '/'.join(both) + ' for dep+arr'
     if dep != []:
         if res != '':
             res += ', '
         res += '%s for departures' % '/'.join(dep)
     if arr != []:
         if res != '':
             res += ', '
         res += '%s for arrivals' % '/'.join(arr)
     return res
예제 #4
0
	def clearFPLs(self, pred=None):
		self.beginResetModel()
		if pred == None:
			self.FPL_list.clear()
		else:
			pop_all(self.FPL_list, lambda fpl: pred(fpl))
		self.endResetModel()
		return True
예제 #5
0
 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)
예제 #6
0
 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())
예제 #7
0
 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
예제 #8
0
	def setupRwyLists(self):
		if env.airport_data != None:
			dep_rwys = [rwy for rwy in env.airport_data.allRunways() if rwy.use_for_departures]
			arr_rwys = [rwy for rwy in env.airport_data.allRunways() if rwy.use_for_arrivals]
			if selection.strip != None:
				acft_type = selection.strip.lookup(FPL.ACFT_TYPE, fpl=True)
				if acft_type != None:
					pop_all(dep_rwys, lambda rwy: not rwy.acceptsAcftType(acft_type))
					pop_all(arr_rwys, lambda rwy: not rwy.acceptsAcftType(acft_type))
			dep_lst = sorted([rwy.name for rwy in dep_rwys] if dep_rwys != [] else env.airport_data.runwayNames())
			arr_lst = sorted([rwy.name for rwy in arr_rwys] if arr_rwys != [] else env.airport_data.runwayNames())
			self.reportReadyRWY_select.clear()
			self.expectArrRWY_select.clear()
			self.reportReadyRWY_select.addItems(dep_lst)
			self.expectArrRWY_select.addItems(arr_lst)
예제 #9
0
 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)
예제 #10
0
    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
예제 #11
0
 def new_arrival_APP(self, entry_point):
     type_choice = self.parkable_aircraft_types if self.parkable_aircraft_types != [] else self.playable_aircraft_types
     # must be landable too
     rwy_choice = [
         rwy for rwy in env.airport_data.allRunways()
         if rwy.use_for_arrivals
     ]
     if rwy_choice == []:
         rwy_choice = env.airport_data.allRunways()
     pop_all(
         type_choice,
         lambda t: all(not rwy.acceptsAcftType(t) for rwy in rwy_choice))
     if type_choice == []:
         return None
     acft_type = choice(type_choice)
     ils = any(rwy.hasILS() for rwy in
               rwy_choice) and random() >= settings.solo_ILSvsVisual_balance
     if entry_point == None:
         hdg = Heading(randint(1, 360), True)
         pos = env.radarPos().moved(
             hdg.opposite(),
             uniform(.33 * settings.radar_range,
                     .75 * settings.radar_range))
     else:
         pos = entry_point.coordinates
         hdg = pos.headingTo(env.radarPos())
     alt = StdPressureAlt.fromFL(
         10 * randint(settings.solo_APP_ceiling_FL_min // 10,
                      settings.solo_APP_ceiling_FL_max // 10))
     if not self.airbornePositionFullySeparated(pos, alt):
         return None
     ias = restrict_speed_under_ceiling(
         cruise_speed(acft_type), alt,
         StdPressureAlt.fromFL(150))  # 5000-ft anticipation
     params = SoloParams(Status(Status.AIRBORNE), pos, alt, hdg, ias)
     params.XPDR_code = env.nextSquawkCodeAssignment(XPDR_range_IFR_ARR)
     return self.mkAiAcft(acft_type, params, ils)
예제 #12
0
 def __init__(self, gui):
     SoloSessionManager.__init__(self, gui)
     pop_all(self.playable_aircraft_types,
             lambda t: acft_cat(t) not in ['jets', 'heavy'])
예제 #13
0
 def forgetStrips(self, pred):
     self.beginResetModel()
     pop_all(self.discarded_strips, lambda elt: pred(elt[0]))
     self.endResetModel()
예제 #14
0
 def deleteClosedWindows(self):
     for w in pop_all(self.child_windows, CpdlcConnectionWidget.closed):
         w.deleteLater()
예제 #15
0
 def dieSignal(self, signal_key):
     pop_all(self.received_signals, lambda sig: sig[0] == signal_key)
     self.signalChanged.emit()
예제 #16
0
 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)
예제 #17
0
    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()
예제 #18
0
 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)
예제 #19
0
 def receiveMsgFromTeacher(self, msg):
     #DEBUG if msg.type != TeachingMsg.TRAFFIC:#
     #DEBUG 	print('=== STUDENT RECEIVES ===\n%s\n=== End ===' % msg.data)
     if msg.type == TeachingMsg.ACFT_KILLED:
         callsign = msg.strData()
         if env.cpdlc.isConnected(callsign):
             env.cpdlc.endDataLink(callsign)
         for acft in pop_all(self.traffic,
                             lambda a: a.identifier == callsign):
             signals.aircraftKilled.emit(acft)
     elif msg.type == TeachingMsg.TRAFFIC:  # traffic update; contains FGMS packet
         fgms_packet = msg.binData()
         update_FgmsAircraft_list(self.traffic, fgms_packet)
         send_packet_to_views(fgms_packet)
         self.teacher.sendMessage(TeachingMsg(TeachingMsg.TRAFFIC))
     elif msg.type == TeachingMsg.SIM_PAUSED:
         self.teacher_paused_at = now()
         signals.sessionPaused.emit()
     elif msg.type == TeachingMsg.SIM_RESUMED:
         pause_delay = now() - self.teacher_paused_at
         for acft in self.traffic:
             acft.moveHistoryTimesForward(pause_delay)
         self.teacher_paused_at = None
         signals.sessionResumed.emit()
     elif msg.type == TeachingMsg.ATC_TEXT_CHAT:
         lines = msg.strData().split('\n')
         if len(lines) == 2:
             signals.incomingAtcTextMsg.emit(
                 ChatMessage(lines[0],
                             lines[1],
                             recipient=student_callsign,
                             private=True))
         else:
             print(
                 'ERROR: Invalid format in received ATC text chat from teacher.'
             )
     elif msg.type == TeachingMsg.STRIP_EXCHANGE:
         line_sep = msg.strData().split('\n', maxsplit=1)
         fromATC = line_sep[0]
         strip = Strip.fromEncodedDetails(
             '' if len(line_sep) < 2 else line_sep[1])
         strip.writeDetail(received_from_detail, fromATC)
         signals.receiveStrip.emit(strip)
     elif msg.type == TeachingMsg.SX_LIST:
         to_remove = set(env.ATCs.knownATCs())
         to_remove.discard(teacher_callsign)
         for line in msg.strData().split('\n'):
             if line != '':  # last line is empty
                 lst = line.rsplit('\t', maxsplit=1)
                 try:
                     frq = CommFrequency(lst[1]) if len(lst) == 2 else None
                 except ValueError:
                     frq = None
                 env.ATCs.updateATC(lst[0], None, None, frq)
                 to_remove.discard(lst[0])
         for atc in to_remove:
             env.ATCs.removeATC(atc)
         env.ATCs.refreshViews()
     elif msg.type == TeachingMsg.WEATHER:
         metar = msg.strData()
         station = metar.split(' ', maxsplit=1)[0]
         if station == settings.primary_METAR_station and metar != self.known_METAR:
             self.known_METAR = metar
             signals.newWeather.emit(station, Weather(metar))
     elif msg.type == TeachingMsg.PTT:  # msg format: "b acft" where b is '1' or '0' for PTT on/off; acft is caller's identifier
         line_sep = msg.strData().split(' ', maxsplit=1)
         try:
             ptt = bool(int(line_sep[0]))
             caller = next(acft for acft in self.getAircraft()
                           if acft.identifier == line_sep[1])
             if ptt:
                 env.rdf.receiveSignal(caller.identifier,
                                       lambda acft=caller: acft.coords())
             else:
                 env.rdf.dieSignal(caller.identifier)
         except StopIteration:
             print('Ignored PTT message from teacher (unknown ACFT %s).' %
                   line_sep[1])
         except (ValueError, IndexError):
             print('Error decoding PTT message value from teacher')
     elif msg.type == TeachingMsg.CPDLC:
         try:
             acft_callsign, line2 = msg.strData().split('\n', maxsplit=1)
             if line2 == '0':  # ACFT is disconnecting
                 env.cpdlc.endDataLink(acft_callsign)
             elif line2 == '1':  # ACFT is connecting (CAUTION: teacher needs confirmation of accepted connection)
                 if settings.controller_pilot_data_link:
                     env.cpdlc.beginDataLink(acft_callsign,
                                             student_callsign)
                 self.teacher.sendMessage(
                     TeachingMsg(
                         TeachingMsg.CPDLC,
                         data=('%s\n%d' %
                               (acft_callsign,
                                settings.controller_pilot_data_link))))
             elif line2.startswith(CPDLC_transfer_cmd_prefix
                                   ):  # ACFT being transferred to me
                 if settings.controller_pilot_data_link:  # CAUTION: teacher needs confirmation of accepted connection
                     xfr_auth = line2[len(CPDLC_transfer_cmd_prefix):]
                     env.cpdlc.beginDataLink(acft_callsign,
                                             student_callsign,
                                             transferFrom=xfr_auth)
                     self.teacher.sendMessage(TeachingMsg(TeachingMsg.CPDLC, \
                       data=('%s\n%s%s' % (acft_callsign, CPDLC_transfer_cmd_prefix, xfr_auth))))
                 else:
                     self.teacher.sendMessage(
                         TeachingMsg(TeachingMsg.CPDLC,
                                     data=('%s\n0' % acft_callsign)))
             elif line2.startswith(
                     CPDLC_message_cmd_prefix):  # ACFT sending a message
                 encoded_msg = line2[len(CPDLC_message_cmd_prefix):]
                 link = env.cpdlc.currentDataLink(acft_callsign)
                 if link == None:
                     print(
                         'Ignored CPDLC message sent from %s while not connected.'
                         % acft_callsign)
                 else:
                     link.appendMessage(
                         CpdlcMessage.fromText(False, encoded_msg))
             else:
                 print('Error decoding CPDLC command from teacher:', line2)
         except (IndexError, ValueError):
             print('Error decoding CPDLC message value from teacher')
     else:
         print('Unhandled message type from teacher: %s' % msg.type)