def handle_l2_payload(self, payload: L2Payload): if payload.destination == AX25Address.from_ax25_call(AX25Call("ID")): print(f"Got ID from {payload.source}: {payload.l3_data}") elif payload.destination == AX25Address.from_ax25_call(AX25Call("CQ")): print(f"Got CQ from {payload.source}: {payload.l3_data}") else: print(f"Unhandled NoLayer3 payload {payload}, dropping.")
def setUp(self): loop = asyncio.get_event_loop() in_queue_1 = asyncio.Queue() out_queue_1 = asyncio.Queue() protocol_factory = partial(KISSProtocol, loop, in_queue_1, out_queue_1, port_id=1, check_crc=False) (transport_1, protocol_1) = utils.create_test_connection(loop, protocol_factory) self.dlm_1 = DataLinkManager(AX25Call("K4DBZ", 1), 1, in_queue_1, out_queue_1, loop.create_future) self.nl_1 = NetworkManager( NetworkConfig.from_dict({ "netrom.node.call": "K4DBZ-1", "netrom.node.alias": "NODE1", "netrom.nodes.interval": 10 }), loop) self.nl_1.attach_data_link(self.dlm_1) self.transport_1 = cast(utils.TestTransport, transport_1) self.protocol_1 = cast(KISSProtocol, protocol_1) in_queue_2 = asyncio.Queue() out_queue_2 = asyncio.Queue() protocol_factory = partial(KISSProtocol, loop, in_queue_2, out_queue_2, port_id=2, check_crc=False) (transport_2, protocol_2) = utils.create_test_connection(loop, protocol_factory) self.dlm_2 = DataLinkManager(AX25Call("K4DBZ", 2), 2, in_queue_2, out_queue_2, loop.create_future) self.nl_2 = NetworkManager( NetworkConfig.from_dict({ "netrom.node.call": "K4DBZ-2", "netrom.node.alias": "NODE2", "netrom.nodes.interval": 10 }), loop) self.nl_2.attach_data_link(self.dlm_2) self.transport_2 = cast(utils.TestTransport, transport_2) self.protocol_2 = cast(KISSProtocol, protocol_2) logger = logging.getLogger("main") logger.setLevel(logging.INFO) handler = logging.StreamHandler(sys.stdout) handler.setFormatter( logging.Formatter('%(levelname)-8s %(asctime)s -- %(message)s')) handler.setLevel(logging.INFO) logger.addHandler(handler)
def maybe_handle_special(self, port: int, packet: AX25Packet) -> bool: if packet.dest == AX25Call("ID") and isinstance(packet, UIFrame): ui = cast(UIFrame, packet) print(f"Got ID from {packet.source}: {ui.info}") return False elif packet.dest == AX25Call("CQ") and isinstance(packet, UIFrame): ui = cast(UIFrame, packet) print(f"Got CQ from {packet.source}: {ui.info}") return False return True
def write(self, address, data): payload = AppPayload(0, 1, bytearray()) ax25_address = AX25Call.parse(address) ax25_address.write(payload.buffer) payload.buffer.extend(data) msg = msgpack.packb(dataclasses.asdict(payload)) self.transport.write(msg)
async def _broadcast_nodes(self): nodes = self.router.get_nodes() nodes.save("nodes.json") for dl in self.data_links.values(): for nodes_packet in nodes.to_packets( AX25Call.parse(self.config.node_call())): dl.write_packet(nodes_packet)
def receive_frame(self, frame: FrameData): try: ax25_packet = decode_ax25_packet(frame.data) intercepted = ax25_packet.dest in self.intercept_calls if intercepted: log_prefix = f"[Port={self.link_port} UDP]" else: log_prefix = f"[Port={self.link_port}]" if ax25_packet.dest == AX25Call("NODES"): packet_logger.debug( f"{log_prefix} RX {len(ax25_packet.buffer)}: {ax25_packet}" ) else: packet_logger.info( f"{log_prefix} RX {len(ax25_packet.buffer)}: {ax25_packet}" ) # Check for port forwarding if intercepted: self.interceptor(frame) return self.maybe_open_link(AX25Address.from_ax25_call( ax25_packet.source)) except Exception: self.exception(f"Had error parsing packet: {frame}") return try: self.state_machine.handle_packet(ax25_packet) except Exception: self.state_machine.reset_state(ax25_packet.source) self.exception(f"Had error handling packet {ax25_packet}")
def load(cls, file: str): if not os.path.exists(file): return None, 0 with open(file) as fp: nodes_json = json.load(fp) sending_alias = nodes_json["nodeAlias"] destinations = [] for dest_json in nodes_json["destinations"]: destinations.append( NodeDestination(AX25Call.parse(dest_json["nodeCall"]), dest_json["nodeAlias"], AX25Call.parse(dest_json["bestNeighbor"]), int(dest_json["quality"]))) return cls(sending_alias, destinations), datetime.datetime.fromisoformat( nodes_json["createdAt"])
def to_packets(self, source: AX25Call) -> Iterator[UIFrame]: for dest_chunk in chunks(self.destinations, 11): nodes_chunk = NetRomNodes(self.sending_alias, dest_chunk) yield UIFrame.ui_frame(AX25Call("NODES", 0), source, [], SupervisoryCommand.Command, False, L3Protocol.NetRom, encode_netrom_nodes(nodes_chunk))
def _loop_sync(self, frame: FrameData): try: packet = decode_ax25_packet(frame.data) if packet.dest == AX25Call("NODES"): self.debug(f"RX: {packet}") else: self.debug(f"RX: {packet}") packet_logger.info(f"RX: {packet}") EventBus.emit("packet", [packet]) except Exception: self.error(f"Had an error parsing packet: {frame}") return try: # Check if this is a special L3 message should_continue = True for l3 in self.l3_handlers: should_continue = l3.maybe_handle_special(frame.port, packet) if not should_continue: self.debug(f"Handled by L3 {l3}") break # If it has not been handled by L3 if should_continue: if not packet.dest == self.link_call: self.warning( f"Discarding packet not for us {packet}. We are {self.link_call}" ) else: self.state_machine.handle_packet(packet) else: self.debug( "Not continuing because this packet was handled by L3") except Exception: self.error(f"Had handling packet {packet}")
def route1(self, destination: L3Address) -> Optional[int]: if not isinstance(destination, l3.NetRomAddress): logger.warning( f"Wrong address family, expected NET/ROM got {destination.__class__}" ) return None netrom_dest = cast(l3.NetRomAddress, destination) packet_dest = AX25Call(netrom_dest.callsign, netrom_dest.ssid) # TODO handle alias here if packet_dest in self.neighbors: logger.debug(f"Routing to neighbor {packet_dest}") return self.neighbors.get(str(packet_dest)).port else: dest = self.destinations.get(str(packet_dest)) if dest: neighbors = dest.sorted_neighbors() logger.debug(f"Sorted neighbors: {neighbors}") if len(neighbors) > 0: neighbor = self.neighbors.get(str(neighbors[0].neighbor)) if neighbor is not None: return neighbor.port else: logger.warning( f"Warning! Routed to dest {packet_dest} via {neighbors[0].neighbor}, " f"but we have no entry for that neighbor!") else: return None else: return None
def write_packet(self, packet: AX25Packet): frame = FrameData(self.link_port, packet.buffer) if packet.dest == AX25Call("NODES"): packet_logger.debug(f"TX: {packet}") else: packet_logger.info(f"TX: {packet}") if not self.queue.offer_outbound(frame): self.warning("Could not send frame, buffer full")
def write_packet(self, packet: AX25Packet): if packet.dest == AX25Call("NODES"): self.debug(f"TX: {packet}") else: self.debug(f"TX: {packet}") packet_logger.info(f"TX: {packet}") frame = FrameData(self.link_port, packet.buffer) asyncio.create_task(self.outbound.put(frame))
def __init__(self, local_call: str, remote_call: str, datalink: DataLinkManager): self.local_call = AX25Call.parse(local_call) self.remote_call = AX25Call.parse(remote_call) self.dl = datalink self.stdin_queue = Queue() self.connected = False self.circuit_id = None EventBus.bind( EventListener(f"link.{local_call}.connect", f"link_{local_call}_connect", self.handle_connect)) EventBus.bind( EventListener(f"link.{local_call}.disconnect", f"link_{local_call}_disconnect", self.handle_disconnect)) EventBus.bind( EventListener(f"link.{local_call}.inbound", f"link_{local_call}_inbound", self.handle_data))
def load(cls, filename: str, node_alias: str): if not os.path.exists(filename): return NetRomRoutingTable(node_alias=node_alias, updated_at=datetime.datetime.now()) d = json_load(filename) return NetRomRoutingTable(node_alias=d["node_alias"], updated_at=datetime.datetime.fromisoformat(d["updated_at"]), our_calls={AX25Call.parse(call) for call in d["our_calls"]}, neighbors={n_dict["call"]: Neighbor.from_safe_dict(n_dict) for n_dict in d["neighbors"]}, destinations={d_dict["call"]: Destination.from_safe_dict(d_dict) for d_dict in d["destinations"]})
def from_safe_dict(cls, d): instance = cls(node_call=AX25Call.parse(d["call"]), node_alias=d["alias"], freeze=d["freeze"]) instance.neighbor_map = { route_dict["neighbor"]: Route.from_safe_dict(route_dict) for route_dict in d["routes"] } return instance
def __init__(self, my_call: str, my_alias: str, remote_call, nl: NetRom): self.local_call = AX25Call.parse(my_call) self.local_alias = AX25Call(callsign=my_alias) self.remote_call = AX25Call.parse(remote_call) self.nl = nl self.stdin_queue = Queue() self.connected = False self.circuit_id = None EventBus.bind( EventListener(f"netrom.{my_call}.connect", f"netrom_{my_call}_connect", self.handle_connect)) EventBus.bind( EventListener(f"netrom.{my_call}.disconnect", f"netrom_{my_call}_disconnect", self.handle_disconnect)) EventBus.bind( EventListener(f"netrom.{my_call}.inbound", f"netrom_{my_call}_inbound", self.handle_data))
def send_nodes_to_link(self, nodes: NetRomNodes, l2: L2Protocol): # not thread safe if isinstance(l2, AX25Protocol): # Special NET/ROM + AX.25 behavior for chunk in nodes.to_chunks(): link_id = l2.maybe_create_logical_link(AX25Call("NODES")) payload = L3Payload(L3Address(), L3Address(), 0xCF, chunk, link_id, QoS.Lowest, reliable=False) packet_logger.debug(f"TX: {payload}") self.link_multiplexer.get_queue(link_id).offer(payload)
async def wait_for_network(): tty = TTY() loop.add_reader(sys.stdin, tty.handle_stdin) loop.add_signal_handler(signal.SIGTERM, tty.handle_signal, loop, scheduler) loop.add_signal_handler(signal.SIGINT, tty.handle_signal, loop, scheduler) remote_call = AX25Call.parse(args.remote_call) while True: (found, mtu) = netrom_l3.route_packet(NetRomAddress.from_call(remote_call)) if found: break await asyncio.sleep(0.200) main_logger.info(f"Learned route to {remote_call}") await asyncio.sleep(1.200) netrom_l4.open(protocol_factory=lambda: tty, local_call=AX25Call.parse(args.local_call), remote_call=AX25Call.parse(args.remote_call), origin_user=AX25Call(args.local_alias), origin_node=AX25Call(args.local_call))
def test_nodes(self): nodes_payload = b'\xffDAVID \x9cf\x98\xa8\xac@\x04DOUG \x96\x9ch\x9e\xa4\x84dz\xaeh\x8a\x92\xa0@dDAVE \x96' \ b'\x9ch\x9e\xa4\x84d_\x96\x9ah\x8a\xa0@\x04JAY \x96\x9ch\x9e\xa4\x84dz\x96\xaeh\x96\xb4@dA' \ b'NOIA \x96\x9ch\x9e\xa4\x84d_\x96\x82d\x88\x8a\xaefFFVC \x96\x9ch\x9e\xa4\x84dz\x96h\x8c' \ b'\x88@@dDAN \x96\x9ch\x9e\xa4\x84d_\x96\x9ah\x92\x8c\xaa\x04ERECH \x96\x9ch\x9e\xa4\x84d' \ b'\x9c\x96h\x92\xa0\x9e@\x04GREG \x96\x9ch\x9e\xa4\x84d_' nodes = parse_netrom_nodes(nodes_payload) assert nodes.sending_alias == "DAVID" re_encoded = encode_netrom_nodes(nodes) assert re_encoded == nodes_payload table = NetRomRoutingTable("DAVID") table.update_routes(AX25Call("K4DBZ", 2), 0, nodes) print(table) routes = table.route(NetRomInfo(AX25Call("K4DBZ", 2), AX25Call("N3LTV", 2), 0, 0, 0, 0, 0, OpType.Information, "Hello, Doug".encode("ascii"))) print(routes) print(table.get_nodes())
async def _handle_packet_async(self, netrom_packet: NetRomPacket): """If packet is for us, handle it, otherwise forward it using our L3 routing table""" EventBus.emit("netrom.incoming", [netrom_packet]) self.debug(f"RX: {netrom_packet}") packet_logger.info(f"RX: {netrom_packet}") if netrom_packet.dest == AX25Call("KEEPLI-0"): # What are these?? Just ignore them pass elif netrom_packet.dest == AX25Call.parse(self.config.node_call()): # Destination is this node self.sm.handle_packet(netrom_packet) elif netrom_packet.dest in self.l3_apps: # Destination is an app served by this node self.sm.handle_packet(netrom_packet) elif netrom_packet.dest in self.l3_servers: # Destination is an app served by this node self.sm.handle_packet(netrom_packet) else: # Destination is somewhere else self.write_packet(netrom_packet, forward=True)
def maybe_handle_special(self, port: int, packet: AX25Packet) -> bool: """L3Handler.maybe_handle_special""" if type(packet) == UIFrame: ui = cast(UIFrame, packet) if ui.protocol == L3Protocol.NetRom and ui.dest == AX25Call( "NODES"): # Parse this NODES packet and mark it as handled nodes = parse_netrom_nodes(ui.info) EventBus.emit("netrom.nodes", [nodes]) asyncio.get_event_loop().create_task( self._update_nodes(packet.source, port, nodes)) # Stop further processing return False return True
def route1(self, destination: L3Address) -> Optional[int]: if not isinstance(destination, l3.NetRomAddress): print(f"Wrong address family, expected NET/ROM got {destination.__class__}") return None netrom_dest = cast(l3.NetRomAddress, destination) packet_dest = AX25Call(netrom_dest.callsign, netrom_dest.ssid) # TODO handle alias here if packet_dest in self.neighbors: return self.neighbors.get(str(packet_dest)).port else: dest = self.destinations.get(str(packet_dest)) if dest: neighbors = dest.sorted_neighbors() if len(neighbors) > 0: return self.neighbors.get(str(neighbors[0].neighbor)).port else: return None else: return None
def receive_frame(self, frame: FrameData): try: ax25_packet = decode_ax25_packet(frame.data) self.maybe_create_logical_link(ax25_packet.source) if ax25_packet.dest == AX25Call("NODES"): packet_logger.info(f"RX: {ax25_packet}") # Eagerly connect to neighbors sending NODES if self.state_machine.get_state( ax25_packet.source) in (AX25StateType.Disconnected, AX25StateType.AwaitingRelease): self.dl_connect_request(copy(ax25_packet.source)) else: packet_logger.debug(f"RX: {ax25_packet}") except Exception: self.exception(f"Had error parsing packet: {frame}") return try: self.state_machine.handle_packet(ax25_packet) except Exception: self.exception(f"Had error handling packet {ax25_packet}")
def _send_cq(self): event = AX25StateEvent.dl_unit_data(AX25Call("CQ"), L3Protocol.NoLayer3, "http://tarpn.net".encode("utf-8")) self.state_machine.handle_internal_event(event) self.cq_timer.reset()
def data_received(self, data: bytes) -> None: s = data.decode("ASCII").strip().upper() self.info(f"Data: {s}") if self.pending_open is not None: self.println(f"Pending connection to {self.pending_open}") # If connected somewhere else, forward the input if self.client_transport: if s == "B" or s == "BYE": self.client_transport.close() self.println( f"Closing connection to {self.client_transport.remote_call}...", True) else: self.client_transport.write(data) return # If not forwarding, parse the command try: parsed_args = self.parser.parse_args(shlex.split(s.lower())) except BaseException: self.println(self.parser.format_help(), True) return if parsed_args.command is None: parsed_args.command = "help" if parsed_args.command == "help": self.println(self.parser.format_help(), True) elif parsed_args.command == "ports": resp = "Ports:\n" for dlm in self.datalinks: resp += f"{dlm.link_port}: {dlm.link_call}\n" self.println(resp, True) elif parsed_args.command == "links": resp = "Links:\n" for dlm in self.datalinks: for remote_call in dlm.state_machine.get_sessions().keys(): if dlm.state_machine.get_state( str(remote_call)) == AX25StateType.Connected: resp += f"L2 {dlm.link_call} > {str(remote_call)} on port {dlm.link_port}\n" for circuit_id in self.network.get_circuit_ids(): circuit = self.network.get_circuit(circuit_id) if circuit.state == NetRomStateType.Connected: resp += f"L3 {circuit.local_call} > {circuit.local_call} on circuit {circuit_id}\n" self.println(resp, True) elif parsed_args.command == "whoami": if isinstance(self.transport, NetworkTransport): nt = cast(NetworkTransport, self.transport) self.println( f"Current user is {nt.origin_user.callsign} connected from {nt.origin_node}", True) else: self.println( f"Current user is default connected from {self.transport.get_extra_info('peername')}", True) elif parsed_args.command == "hostname": self.println(f"Current host is {self.network.local_call()}", True) elif parsed_args.command == "bye": self.println("Goodbye.") self.transport.close() elif parsed_args.command == "connect": """ Connect to a remote station Create a half-opened client connection to the remote station. Once the connect ack is received, the connection will be completed and we will create the protocol and transport objects. """ remote_call = AX25Call.parse(parsed_args.dest) local_call = AX25Call.parse( self.settings.network_configs().node_call()) self.println(f"Connecting to {remote_call}...", True) if isinstance(self.transport, NetworkTransport): nt = cast(NetworkTransport, self.transport) self.network.open(partial(ConnectProtocol, self), local_call, remote_call, nt.origin_node, nt.origin_user) self.pending_open = remote_call else: logging.warning( f"Connect command is only supported for NetworkTransport, not {self.transport.__class__}" ) self.println( f"Connect command is only supported for NetworkTransport", True) else: logging.warning(f"Unhandled command {parsed_args.command}") self.println(f"Unhandled command {parsed_args.command}", True)
def main(): parser = argparse.ArgumentParser(description='Decode packets from a serial port') parser.add_argument("port", help="Serial port to open") parser.add_argument("baud", type=int, help="Baudrate to use") parser.add_argument("local_call", help="Your callsign (e.g., K4DBZ-10)") parser.add_argument("local_alias", help="Your alias (e.g., ZDBZ10)") parser.add_argument("remote_call", help="Remote callsign") parser.add_argument("-datalink", help="Force L2 mode", action="store_true") parser.add_argument("--check-crc", type=bool, default=False) parser.add_argument("--monitor-port", type=int) parser.add_argument("--debug", action="store_true") args = parser.parse_args() # Configure logging main_logger = logging.getLogger("root") main_logger.setLevel(logging.ERROR) if args.debug: main_logger.setLevel(logging.DEBUG) state_logger = logging.getLogger("ax25.state") state_logger.setLevel(logging.DEBUG) state_logger.addHandler(logging.StreamHandler(sys.stdout)) state_logger = logging.getLogger("netrom.state") state_logger.setLevel(logging.DEBUG) state_logger.addHandler(logging.StreamHandler(sys.stdout)) scheduler = Scheduler() # Configure and initialize I/O device and L2 port_config = PortConfig.from_dict(0, { "port.enabled": True, "port.type": "serial", "serial.device": args.port, "serial.speed": args.baud }) # Initialize I/O device and L2 l3_protocols = L3Protocols() l2_multi = DefaultLinkMultiplexer(L3PriorityQueue, scheduler) l2_queueing = L2FIFOQueue(20, AX25Protocol.maximum_frame_size()) l2 = AX25Protocol(port_config, port_config.port_id(), AX25Call.parse(args.local_call), scheduler, l2_queueing, l2_multi, l3_protocols) kiss = KISSProtocol(port_config.port_id(), l2_queueing, port_config.get_boolean("kiss.checksum", False)) SerialDevice(kiss, port_config.get("serial.device"), port_config.get_int("serial.speed"), port_config.get_float("serial.timeout"), scheduler) scheduler.submit(L2IOLoop(l2_queueing, l2)) # Initialize L3 and L4 network_config = NetworkConfig.from_dict({ "netrom.node.call": args.local_call, "netrom.node.alias": args.local_alias, "netrom.ttl": 7, "netrom.nodes.interval": 60, "netrom.obs.init": 6, "netrom.obs.min": 4, "netrom.nodes.quality.min": 74 }) # Register L3 protocols netrom_l3 = NetRomL3(AX25Call.parse(network_config.node_call()), network_config.node_alias(), scheduler, l2_multi, NetRomRoutingTable(network_config.node_alias())) l3_protocols.register(netrom_l3) # Create the L4 protocol netrom_l4 = NetRomTransportProtocol(network_config, netrom_l3, scheduler) async def wait_for_network(): tty = TTY() loop.add_reader(sys.stdin, tty.handle_stdin) loop.add_signal_handler(signal.SIGTERM, tty.handle_signal, loop, scheduler) loop.add_signal_handler(signal.SIGINT, tty.handle_signal, loop, scheduler) remote_call = AX25Call.parse(args.remote_call) while True: (found, mtu) = netrom_l3.route_packet(NetRomAddress.from_call(remote_call)) if found: break await asyncio.sleep(0.200) main_logger.info(f"Learned route to {remote_call}") await asyncio.sleep(1.200) netrom_l4.open(protocol_factory=lambda: tty, local_call=AX25Call.parse(args.local_call), remote_call=AX25Call.parse(args.remote_call), origin_user=AX25Call(args.local_alias), origin_node=AX25Call(args.local_call)) loop = asyncio.get_event_loop() loop.create_task(wait_for_network()) try: loop.run_forever() finally: loop.close()
def local_call(self) -> AX25Call: return AX25Call.parse(self.config.node_call())
def to_ax25_call(self): return AX25Call(self.callsign, self.ssid)
async def main_async(): parser = argparse.ArgumentParser( description='Decode packets from a serial port') parser.add_argument("config", help="Config file") parser.add_argument("--debug", action="store_true") args = parser.parse_args() s = Settings(".", args.config) node_settings = s.node_config() # Create the main event loop loop = asyncio.get_event_loop() dlms = [] for port_config in s.port_configs(): in_queue: asyncio.Queue = asyncio.Queue() out_queue: asyncio.Queue = asyncio.Queue() asyncio.create_task(kiss_port_factory(in_queue, out_queue, port_config)) # Wire the port with an AX25 layer dlm = DataLinkManager(AX25Call.parse(node_settings.node_call()), port_config.port_id(), in_queue, out_queue, loop.create_future) dlms.append(dlm) # Wire up Layer 3 and default L2 app nl = NetworkManager(s.network_configs(), loop) for dlm in dlms: nl.attach_data_link(dlm) dlm.add_l3_handler(IdHandler()) # Bind apps to netrom and start running the app servers for app_config in s.app_configs(): # This multiplexer bridges the unix socket server and the network connections multiplexer = TransportMultiplexer() # We have a single unix socket connection unix_factory = partial(NetromAppProtocol, app_config.app_name(), AX25Call.parse(app_config.app_call()), app_config.app_alias(), nl, multiplexer) logger.info( f"Creating unix socket server for {app_config.app_call()} at {app_config.app_socket()}" ) await loop.create_unix_server(unix_factory, app_config.app_socket(), start_serving=True) # And many network connections network_factory = partial(MultiplexingProtocol, multiplexer) nl.bind_server(AX25Call.parse(app_config.app_call()), app_config.app_alias(), network_factory) node_app_factory = partial(CommandProcessorProtocol, s, dlms, nl) for dlm in dlms: # TODO add a bind_server thing here too? dlm.add_l3_handler(DataLinkAdapter(dlm, node_app_factory)) node_call = s.network_configs().node_call() node_alias = s.network_configs().node_alias() # Make a default application for L4 nl.bind_server(AX25Call.parse(node_call), node_alias, node_app_factory) if node_settings.admin_enabled(): await loop.create_server(protocol_factory=node_app_factory, host=node_settings.admin_listen(), port=node_settings.admin_port(), start_serving=True) # Configure logging logging.config.fileConfig("config/logging.ini", disable_existing_loggers=False) event_logger = logging.getLogger("events") event_logger.setLevel(logging.INFO) event_logger.addHandler(handler) # Start processing packets tasks = [dlm.start() for dlm in dlms] tasks.append(nl.start()) logger.info("Packet engine started") await asyncio.wait(tasks)
def from_safe_dict(cls, d): return cls(neighbor=AX25Call.parse(d["neighbor"]), dest=AX25Call.parse(d["destination"]), next_hop=AX25Call.parse(d["next_hop"]), quality=d["quality"], obsolescence=d["obsolescence"])