From b53e2ba7fe72bba9e2d121a28edb55a25ca0c648 Mon Sep 17 00:00:00 2001 From: Hemna Date: Thu, 25 Feb 2021 21:01:52 -0500 Subject: [PATCH 1/4] Added the ability to use direwolf KISS socket This patch adds APRS KISS connectivity. I have tested this with a running Direwolf install via either a serial KISS connection or the optional new TCPKISS connection, both to Direwolf. This adds the new required aioax25 python library for the underlying KISS and AX25 support. NOTE: For the TCPKISS connection, this patch requires a pull request patch the aioax25 library to include a TCP Based KISS TNC client to enable the TCPKISS client So you will need to pull down this PR https://github.com/sjlongland/aioax25/pull/7 To enable this, Edit your aprsd.yml file and enable one of the 2 KISS connections. Only one is supported at a time. kiss: serial: enabled: True device: /dev/ttyS1 baudrate: 9600 or kiss: tcp: enabled: True host: "ip address/hostname of direwolf" port: "direwolf configured kiss port" This patch alters the Message object classes to be able to send messages out via the aprslib socket connection to the APRS-IS network on the internet, or via the direwolf KISS TCP socket, depending on the origination of the initial message coming in. If an APRS message comes in via APRS-IS, then replies will go out APRS-IS. IF an APRS message comes in via direwolf, then replies will go out via direwolf KISS TCP socket. Both can work at the same time. TODO: I need some real APRS message packets to verify that the new thread is processing packets correctly through the plugins and able to send the resulting messages back out to direwolf. Have a hard coded callsign for now in the kissclient consumer call, just so I can see messages coming in from direwolf. I dont' have an APRS capable radio at the moment to send messages directly to direwolf. Might need to write a simple python socket server to send fake APRS messages to aprsd kiss, just for finishing up development. --- aprsd/client.py | 5 ++ aprsd/kissclient.py | 138 ++++++++++++++++++++++++++++++++++ aprsd/main.py | 28 +++++-- aprsd/messaging.py | 62 +++++++++++----- aprsd/threads.py | 175 +++++++++++++++++++++++++++++++++++++++++++- aprsd/utils.py | 18 ++++- requirements.in | 1 + requirements.txt | 11 +++ 8 files changed, 411 insertions(+), 27 deletions(-) create mode 100644 aprsd/kissclient.py diff --git a/aprsd/client.py b/aprsd/client.py index 40c9d40..a2d5e45 100644 --- a/aprsd/client.py +++ b/aprsd/client.py @@ -90,6 +90,11 @@ class Aprsdis(aprslib.IS): self.thread_stop = True LOG.info("Shutdown Aprsdis client.") + def send(self, msg): + """Send an APRS Message object.""" + line = str(msg) + self.sendall(line) + def _socket_readlines(self, blocking=False): """ Generator for complete lines, received from the server diff --git a/aprsd/kissclient.py b/aprsd/kissclient.py new file mode 100644 index 0000000..e3e707a --- /dev/null +++ b/aprsd/kissclient.py @@ -0,0 +1,138 @@ +import asyncio +import logging + +from aioax25 import frame as axframe +from aioax25 import interface +from aioax25 import kiss as kiss +from aioax25.aprs import APRSInterface +from aprsd import trace + +LOG = logging.getLogger("APRSD") + + +class KISSClient: + + _instance = None + config = None + ax25client = None + loop = None + + def __new__(cls, *args, **kwargs): + """Singleton for this class.""" + if cls._instance is None: + cls._instance = super().__new__(cls) + # initialize shit here + return cls._instance + + def __init__(self, config=None): + if config: + self.config = config + + @staticmethod + def kiss_enabled(config): + """Return if tcp or serial KISS is enabled.""" + if "serial" in config["kiss"]: + if config["kiss"]["serial"].get("enabled", False): + return True + + if "tcp" in config["kiss"]: + if config["kiss"]["serial"].get("enabled", False): + return True + + @property + def client(self): + if not self.ax25client: + self.ax25client = self.setup_connection() + return self.ax25client + + def reset(self): + """Call this to fore a rebuild/reconnect.""" + self.ax25client.stop() + del self.ax25client + + @trace.trace + def setup_connection(self): + ax25client = Aioax25Client(self.config) + LOG.debug("Complete") + return ax25client + + +class Aioax25Client: + def __init__(self, config): + self.config = config + self.setup() + + def setup(self): + # we can be TCP kiss or Serial kiss + + self.loop = asyncio.get_event_loop() + if "serial" in self.config["kiss"] and self.config["kiss"]["serial"].get( + "enabled", + False, + ): + LOG.debug( + "Setting up Serial KISS connection to {}".format( + self.config["kiss"]["serial"]["device"], + ), + ) + self.kissdev = kiss.SerialKISSDevice( + device=self.config["kiss"]["serial"]["device"], + baudrate=self.config["kiss"]["serial"].get("baudrate", 9600), + loop=self.loop, + ) + elif "tcp" in self.config["kiss"] and self.config["kiss"]["tcp"].get( + "enabled", + False, + ): + LOG.debug( + "Setting up KISSTCP Connection to {}:{}".format( + self.config["kiss"]["host"], + self.config["kiss"]["port"], + ), + ) + self.kissdev = kiss.TCPKISSDevice( + self.config["kiss"]["host"], + self.config["kiss"]["port"], + loop=self.loop, + ) + + self.kissdev.open() + self.kissport0 = self.kissdev[0] + + LOG.debug("Creating AX25Interface") + self.ax25int = interface.AX25Interface(kissport=self.kissport0, loop=self.loop) + + LOG.debug("Creating APRSInterface") + self.aprsint = APRSInterface( + ax25int=self.ax25int, + mycall=self.config["ham"]["callsign"], + log=LOG, + ) + + def stop(self): + LOG.debug(self.kissdev) + self.kissdev._close() + self.loop.stop() + + def consumer(self, callback, callsign=None): + if not callsign: + callsign = self.config["ham"]["callsign"] + self.aprsint.bind(callback=callback, callsign=callsign, regex=True) + + def send(self, msg): + """Send an APRS Message object.""" + payload = msg._filter_for_send() + frame = axframe.AX25UnnumberedInformationFrame( + msg.tocall, + msg.fromcall.encode("UTF-8"), + pid=0xF0, + repeaters=b"WIDE2-1", + payload=payload, + ) + LOG.debug(frame) + self.ax25int.transmit(frame) + + +def get_client(): + cl = KISSClient() + return cl.client diff --git a/aprsd/main.py b/aprsd/main.py index d7ee309..aa0674a 100644 --- a/aprsd/main.py +++ b/aprsd/main.py @@ -37,7 +37,7 @@ import click_completion # local imports here import aprsd from aprsd import ( - client, flask, messaging, packets, plugin, stats, threads, trace, utils, + client, flask, kissclient, messaging, packets, plugin, stats, threads, trace, utils, ) @@ -458,16 +458,22 @@ def server( trace.setup_tracing(["method", "api"]) stats.APRSDStats(config) - try: - cl = client.Client(config) - cl.client - except LoginError: - sys.exit(-1) - # Create the initial PM singleton and Register plugins plugin_manager = plugin.PluginManager(config) plugin_manager.setup_plugins() + if config["aprs"].get("enabled", True): + try: + cl = client.Client(config) + cl.client + except LoginError: + sys.exit(-1) + else: + LOG.info( + "APRS network connection Not Enabled in config. This is" + " for setups without internet connectivity.", + ) + # Now load the msgTrack from disk if any if flush: LOG.debug("Deleting saved MsgTrack.") @@ -492,6 +498,14 @@ def server( ): packets.WatchList(config=config) + if kissclient.KISSClient.kiss_enabled(config): + kcl = kissclient.KISSClient(config=config) + kcl.client + + kissrx_thread = threads.KISSRXThread(msg_queues=msg_queues, config=config) + kissrx_thread.start() + + messaging.MsgTrack().restart() keepalive = threads.KeepAliveThread(config=config) diff --git a/aprsd/messaging.py b/aprsd/messaging.py index 1d2c8b3..8186813 100644 --- a/aprsd/messaging.py +++ b/aprsd/messaging.py @@ -9,7 +9,7 @@ import re import threading import time -from aprsd import client, packets, stats, threads, trace, utils +from aprsd import client, kissclient, packets, stats, threads, trace, utils LOG = logging.getLogger("APRSD") @@ -18,6 +18,9 @@ LOG = logging.getLogger("APRSD") # and it's ok, but don't send a usage string back NULL_MESSAGE = -1 +MESSAGE_TRANSPORT_TCPKISS = "tcpkiss" +MESSAGE_TRANSPORT_APRSIS = "aprsis" + class MsgTrack: """Class to keep track of outstanding text messages. @@ -228,7 +231,15 @@ class Message(metaclass=abc.ABCMeta): last_send_time = 0 last_send_attempt = 0 - def __init__(self, fromcall, tocall, msg_id=None): + transport = None + + def __init__( + self, + fromcall, + tocall, + msg_id=None, + transport=MESSAGE_TRANSPORT_APRSIS, + ): self.fromcall = fromcall self.tocall = tocall if not msg_id: @@ -236,11 +247,18 @@ class Message(metaclass=abc.ABCMeta): c.increment() msg_id = c.value self.id = msg_id + self.transport = transport @abc.abstractmethod def send(self): """Child class must declare.""" + def get_transport(self): + if self.transport == MESSAGE_TRANSPORT_APRSIS: + return client.get_client() + elif self.transport == MESSAGE_TRANSPORT_TCPKISS: + return kissclient.get_client() + class RawMessage(Message): """Send a raw message. @@ -252,8 +270,8 @@ class RawMessage(Message): message = None - def __init__(self, message): - super().__init__(None, None, msg_id=None) + def __init__(self, message, transport=MESSAGE_TRANSPORT_APRSIS): + super().__init__(None, None, msg_id=None, transport=transport) self.message = message def dict(self): @@ -282,7 +300,7 @@ class RawMessage(Message): def send_direct(self): """Send a message without a separate thread.""" - cl = client.get_client() + cl = self.get_transport() log_message( "Sending Message Direct", str(self).rstrip("\n"), @@ -290,7 +308,7 @@ class RawMessage(Message): tocall=self.tocall, fromcall=self.fromcall, ) - cl.sendall(str(self)) + cl.send(self) stats.APRSDStats().msgs_sent_inc() @@ -299,8 +317,16 @@ class TextMessage(Message): message = None - def __init__(self, fromcall, tocall, message, msg_id=None, allow_delay=True): - super().__init__(fromcall, tocall, msg_id) + def __init__( + self, + fromcall, + tocall, + message, + msg_id=None, + allow_delay=True, + transport=MESSAGE_TRANSPORT_APRSIS, + ): + super().__init__(fromcall, tocall, msg_id, transport=transport) self.message = message # do we try and save this message for later if we don't get # an ack? Some messages we don't want to do this ever. @@ -354,7 +380,7 @@ class TextMessage(Message): def send_direct(self): """Send a message without a separate thread.""" - cl = client.get_client() + cl = self.get_transport() log_message( "Sending Message Direct", str(self).rstrip("\n"), @@ -362,7 +388,7 @@ class TextMessage(Message): tocall=self.tocall, fromcall=self.fromcall, ) - cl.sendall(str(self)) + cl.send(self) stats.APRSDStats().msgs_tx_inc() @@ -382,7 +408,6 @@ class SendMessageThread(threads.APRSDThread): last send attempt is old enough. """ - cl = client.get_client() tracker = MsgTrack() # lets see if the message is still in the tracking queue msg = tracker.get(self.msg.id) @@ -392,6 +417,7 @@ class SendMessageThread(threads.APRSDThread): LOG.info("Message Send Complete via Ack.") return False else: + cl = msg.get_transport() send_now = False if msg.last_send_attempt == msg.retry_count: # we reached the send limit, don't send again @@ -422,7 +448,7 @@ class SendMessageThread(threads.APRSDThread): retry_number=msg.last_send_attempt, msg_num=msg.id, ) - cl.sendall(str(msg)) + cl.send(msg) stats.APRSDStats().msgs_tx_inc() packets.PacketList().add(msg.dict()) msg.last_send_time = datetime.datetime.now() @@ -436,8 +462,8 @@ class SendMessageThread(threads.APRSDThread): class AckMessage(Message): """Class for building Acks and sending them.""" - def __init__(self, fromcall, tocall, msg_id): - super().__init__(fromcall, tocall, msg_id=msg_id) + def __init__(self, fromcall, tocall, msg_id, transport=MESSAGE_TRANSPORT_APRSIS): + super().__init__(fromcall, tocall, msg_id=msg_id, transport=transport) def dict(self): now = datetime.datetime.now() @@ -470,7 +496,7 @@ class AckMessage(Message): def send_direct(self): """Send an ack message without a separate thread.""" - cl = client.get_client() + cl = self.get_transport() log_message( "Sending ack", str(self).rstrip("\n"), @@ -479,7 +505,7 @@ class AckMessage(Message): tocall=self.tocall, fromcall=self.fromcall, ) - cl.sendall(str(self)) + cl.send(self) class SendAckThread(threads.APRSDThread): @@ -515,7 +541,7 @@ class SendAckThread(threads.APRSDThread): send_now = True if send_now: - cl = client.get_client() + cl = self.ack.get_transport() log_message( "Sending ack", str(self.ack).rstrip("\n"), @@ -524,7 +550,7 @@ class SendAckThread(threads.APRSDThread): tocall=self.ack.tocall, retry_number=self.ack.last_send_attempt, ) - cl.sendall(str(self.ack)) + cl.send(self.ack) stats.APRSDStats().ack_tx_inc() packets.PacketList().add(self.ack.dict()) self.ack.last_send_attempt += 1 diff --git a/aprsd/threads.py b/aprsd/threads.py index e1589bb..8caaebf 100644 --- a/aprsd/threads.py +++ b/aprsd/threads.py @@ -8,7 +8,7 @@ import tracemalloc import aprslib -from aprsd import client, messaging, packets, plugin, stats, utils +from aprsd import client, kissclient, messaging, packets, plugin, stats, utils LOG = logging.getLogger("APRSD") @@ -314,3 +314,176 @@ class APRSDTXThread(APRSDThread): pass # Continue to loop return True + + +class KISSRXThread(APRSDThread): + """Thread that connects to direwolf's TCPKISS interface. + + All Packets are processed and sent back out the direwolf + interface instead of the aprs-is server. + + """ + + def __init__(self, msg_queues, config): + super().__init__("KISSRX_MSG") + self.msg_queues = msg_queues + self.config = config + + def stop(self): + self.thread_stop = True + kissclient.get_client().stop() + + def loop(self): + kiss_client = kissclient.get_client() + + # setup the consumer of messages and block until a messages + try: + # This will register a packet consumer with aprslib + # When new packets come in the consumer will process + # the packet + + # Do a partial here because the consumer signature doesn't allow + # For kwargs to be passed in to the consumer func we declare + # and the aprslib developer didn't want to allow a PR to add + # kwargs. :( + # https://github.com/rossengeorgiev/aprs-python/pull/56 + kiss_client.consumer(self.process_packet, callsign="APN382") + kiss_client.loop.run_forever() + + except aprslib.exceptions.ConnectionDrop: + LOG.error("Connection dropped, reconnecting") + time.sleep(5) + # Force the deletion of the client object connected to aprs + # This will cause a reconnect, next time client.get_client() + # is called + client.Client().reset() + # Continue to loop + + @trace.trace + def process_packet(self, interface, frame, match): + """Process a packet recieved from aprs-is server.""" + + LOG.debug("Got an APRS Frame '{}'".format(frame)) + + payload = str(frame.payload.decode()) + msg = "{}:{}".format(str(frame.header), payload) + + packet = aprslib.parse(msg) + LOG.debug(packet) + + try: + stats.APRSDStats().msgs_rx_inc() + + msg = packet.get("message_text", None) + msg_format = packet.get("format", None) + msg_response = packet.get("response", None) + if msg_format == "message" and msg: + # we want to send the message through the + # plugins + self.process_message_packet(packet) + return + elif msg_response == "ack": + self.process_ack_packet(packet) + return + + if msg_format == "mic-e": + # process a mic-e packet + self.process_mic_e_packet(packet) + return + + except (aprslib.ParseError, aprslib.UnknownFormat) as exp: + LOG.exception("Failed to parse packet from aprs-is", exp) + + @trace.trace + def process_message_packet(self, packet): + LOG.debug("Message packet rx") + fromcall = packet["from"] + message = packet.get("message_text", None) + msg_id = packet.get("msgNo", "0") + messaging.log_message( + "Received Message", + packet["raw"], + message, + fromcall=fromcall, + msg_num=msg_id, + ) + found_command = False + # Get singleton of the PM + pm = plugin.PluginManager() + try: + results = pm.run(fromcall=fromcall, message=message, ack=msg_id) + for reply in results: + found_command = True + # A plugin can return a null message flag which signals + # us that they processed the message correctly, but have + # nothing to reply with, so we avoid replying with a usage string + if reply is not messaging.NULL_MESSAGE: + LOG.debug("Sending '{}'".format(reply)) + + msg = messaging.TextMessage( + self.config["aprs"]["login"], + fromcall, + reply, + transport=messaging.MESSAGE_TRANSPORT_TCPKISS, + ) + self.msg_queues["tx"].put(msg) + else: + LOG.debug("Got NULL MESSAGE from plugin") + + if not found_command: + plugins = pm.get_plugins() + names = [x.command_name for x in plugins] + names.sort() + + # reply = "Usage: {}".format(", ".join(names)) + reply = "Usage: weather, locate [call], time, fortune, ping" + + msg = messaging.TextMessage( + self.config["aprs"]["login"], + fromcall, + reply, + transport=messaging.MESSAGE_TRANSPORT_TCPKISS, + ) + self.msg_queues["tx"].put(msg) + except Exception as ex: + LOG.exception("Plugin failed!!!", ex) + reply = "A Plugin failed! try again?" + msg = messaging.TextMessage( + self.config["aprs"]["login"], + fromcall, + reply, + transport=messaging.MESSAGE_TRANSPORT_TCPKISS, + ) + self.msg_queues["tx"].put(msg) + + # let any threads do their thing, then ack + # send an ack last + ack = messaging.AckMessage( + self.config["aprs"]["login"], + fromcall, + msg_id=msg_id, + transport=messaging.MESSAGE_TRANSPORT_TCPKISS, + ) + self.msg_queues["tx"].put(ack) + LOG.debug("Packet processing complete") + + def process_ack_packet(self, packet): + ack_num = packet.get("msgNo") + LOG.info("Got ack for message {}".format(ack_num)) + messaging.log_message( + "ACK", + packet["raw"], + None, + ack=ack_num, + fromcall=packet["from"], + ) + tracker = messaging.MsgTrack() + tracker.remove(ack_num) + stats.APRSDStats().ack_rx_inc() + return + + def process_mic_e_packet(self, packet): + LOG.info("Mic-E Packet detected. Currenlty unsupported.") + messaging.log_packet(packet) + stats.APRSDStats().msgs_mice_inc() + return diff --git a/aprsd/utils.py b/aprsd/utils.py index 14b47ff..e2acd2b 100644 --- a/aprsd/utils.py +++ b/aprsd/utils.py @@ -37,11 +37,24 @@ DEFAULT_DATE_FORMAT = "%m/%d/%Y %I:%M:%S %p" DEFAULT_CONFIG_DICT = { "ham": {"callsign": "NOCALL"}, "aprs": { - "login": "NOCALL", + "enabled": True, + "login": "CALLSIGN", "password": "00000", "host": "rotate.aprs2.net", "port": 14580, }, + "kiss": { + "tcp": { + "enabled": False, + "host": "direwolf.ip.address", + "port": "8001", + }, + "serial": { + "enabled": False, + "device": "/dev/ttyS0", + "baudrate": 9600, + }, + }, "aprsd": { "logfile": "/tmp/aprsd.log", "logformat": DEFAULT_LOG_FORMAT, @@ -172,6 +185,9 @@ def add_config_comments(raw_yaml): # lets insert a comment raw_yaml = insert_str( raw_yaml, + "\n # Set enabled to False if there is no internet connectivity." + "\n # This is useful for a direwolf KISS aprs connection only. " + "\n" "\n # Get the passcode for your callsign here: " "\n # https://apps.magicbug.co.uk/passcode", end_idx, diff --git a/requirements.in b/requirements.in index 3a6f437..7d96198 100644 --- a/requirements.in +++ b/requirements.in @@ -1,3 +1,4 @@ +aioax25 aprslib click click-completion diff --git a/requirements.txt b/requirements.txt index 9e5af8c..c02df9e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,6 +4,8 @@ # # pip-compile requirements.in # +aioax25==0.0.9 + # via -r requirements.in aprslib==0.6.47 # via -r requirements.in backoff==1.10.0 @@ -21,6 +23,8 @@ click==7.1.2 # -r requirements.in # click-completion # flask +contexter==0.1.4 + # via signalslot cryptography==3.4.7 # via pyopenssl dnspython==2.1.0 @@ -72,6 +76,8 @@ pycparser==2.20 # via cffi pyopenssl==20.0.1 # via opencage +pyserial==3.5 + # via aioax25 python-dateutil==2.8.1 # via pandas pytz==2021.1 @@ -88,6 +94,8 @@ requests==2.25.1 # yfinance shellingham==1.4.0 # via click-completion +signalslot==0.1.2 + # via aioax25 six==1.15.0 # via # -r requirements.in @@ -96,12 +104,15 @@ six==1.15.0 # opencage # pyopenssl # python-dateutil + # signalslot thesmuggler==1.0.1 # via -r requirements.in update-checker==0.18.0 # via -r requirements.in urllib3==1.26.5 # via requests +weakrefmethod==1.0.3 + # via signalslot werkzeug==1.0.1 # via flask yfinance==0.1.59 From 54c9a6b55a8a102d44109ae454b4c76240ae0780 Mon Sep 17 00:00:00 2001 From: Hemna Date: Mon, 30 Aug 2021 13:34:25 -0400 Subject: [PATCH 2/4] Rebased from master --- aprsd/kissclient.py | 2 ++ aprsd/main.py | 7 ++++--- aprsd/threads.py | 12 +++++++----- 3 files changed, 13 insertions(+), 8 deletions(-) diff --git a/aprsd/kissclient.py b/aprsd/kissclient.py index e3e707a..9bd5d80 100644 --- a/aprsd/kissclient.py +++ b/aprsd/kissclient.py @@ -5,8 +5,10 @@ from aioax25 import frame as axframe from aioax25 import interface from aioax25 import kiss as kiss from aioax25.aprs import APRSInterface + from aprsd import trace + LOG = logging.getLogger("APRSD") diff --git a/aprsd/main.py b/aprsd/main.py index aa0674a..05099e7 100644 --- a/aprsd/main.py +++ b/aprsd/main.py @@ -37,7 +37,8 @@ import click_completion # local imports here import aprsd from aprsd import ( - client, flask, kissclient, messaging, packets, plugin, stats, threads, trace, utils, + client, flask, kissclient, messaging, packets, plugin, stats, threads, + trace, utils, ) @@ -500,12 +501,12 @@ def server( if kissclient.KISSClient.kiss_enabled(config): kcl = kissclient.KISSClient(config=config) + # This initializes the client object. kcl.client - kissrx_thread = threads.KISSRXThread(msg_queues=msg_queues, config=config) + kissrx_thread = threads.KISSRXThread(msg_queues=threads.msg_queues, config=config) kissrx_thread.start() - messaging.MsgTrack().restart() keepalive = threads.KeepAliveThread(config=config) diff --git a/aprsd/threads.py b/aprsd/threads.py index 8caaebf..bd7a4c5 100644 --- a/aprsd/threads.py +++ b/aprsd/threads.py @@ -8,7 +8,9 @@ import tracemalloc import aprslib -from aprsd import client, kissclient, messaging, packets, plugin, stats, utils +from aprsd import ( + client, kissclient, messaging, packets, plugin, stats, trace, utils, +) LOG = logging.getLogger("APRSD") @@ -363,10 +365,10 @@ class KISSRXThread(APRSDThread): def process_packet(self, interface, frame, match): """Process a packet recieved from aprs-is server.""" - LOG.debug("Got an APRS Frame '{}'".format(frame)) + LOG.debug(f"Got an APRS Frame '{frame}'") payload = str(frame.payload.decode()) - msg = "{}:{}".format(str(frame.header), payload) + msg = f"{str(frame.header)}:{payload}" packet = aprslib.parse(msg) LOG.debug(packet) @@ -418,7 +420,7 @@ class KISSRXThread(APRSDThread): # us that they processed the message correctly, but have # nothing to reply with, so we avoid replying with a usage string if reply is not messaging.NULL_MESSAGE: - LOG.debug("Sending '{}'".format(reply)) + LOG.debug(f"Sending '{reply}'") msg = messaging.TextMessage( self.config["aprs"]["login"], @@ -469,7 +471,7 @@ class KISSRXThread(APRSDThread): def process_ack_packet(self, packet): ack_num = packet.get("msgNo") - LOG.info("Got ack for message {}".format(ack_num)) + LOG.info(f"Got ack for message {ack_num}") messaging.log_message( "ACK", packet["raw"], From f4dee4b202c3b29e265f340ae50f2f850f3fb1c8 Mon Sep 17 00:00:00 2001 From: Hemna Date: Wed, 1 Sep 2021 14:48:22 -0400 Subject: [PATCH 3/4] Got TX/RX working with aioax25+direwolf over TCP This patch gets APRSD fully working with the TCPKISS socket to direwolf. --- aprsd/kissclient.py | 31 +++++----- aprsd/main.py | 13 ++--- aprsd/messaging.py | 3 + aprsd/threads.py | 138 +++++--------------------------------------- requirements.in | 2 +- requirements.txt | 2 +- 6 files changed, 41 insertions(+), 148 deletions(-) diff --git a/aprsd/kissclient.py b/aprsd/kissclient.py index 9bd5d80..3db801e 100644 --- a/aprsd/kissclient.py +++ b/aprsd/kissclient.py @@ -1,7 +1,6 @@ import asyncio import logging -from aioax25 import frame as axframe from aioax25 import interface from aioax25 import kiss as kiss from aioax25.aprs import APRSInterface @@ -38,7 +37,7 @@ class KISSClient: return True if "tcp" in config["kiss"]: - if config["kiss"]["serial"].get("enabled", False): + if config["kiss"]["tcp"].get("enabled", False): return True @property @@ -88,14 +87,15 @@ class Aioax25Client: ): LOG.debug( "Setting up KISSTCP Connection to {}:{}".format( - self.config["kiss"]["host"], - self.config["kiss"]["port"], + self.config["kiss"]["tcp"]["host"], + self.config["kiss"]["tcp"]["port"], ), ) self.kissdev = kiss.TCPKISSDevice( - self.config["kiss"]["host"], - self.config["kiss"]["port"], + self.config["kiss"]["tcp"]["host"], + self.config["kiss"]["tcp"]["port"], loop=self.loop, + log=LOG, ) self.kissdev.open() @@ -107,7 +107,7 @@ class Aioax25Client: LOG.debug("Creating APRSInterface") self.aprsint = APRSInterface( ax25int=self.ax25int, - mycall=self.config["ham"]["callsign"], + mycall=self.config["kiss"]["callsign"], log=LOG, ) @@ -119,20 +119,17 @@ class Aioax25Client: def consumer(self, callback, callsign=None): if not callsign: callsign = self.config["ham"]["callsign"] - self.aprsint.bind(callback=callback, callsign=callsign, regex=True) + self.aprsint.bind(callback=callback, callsign="WB4BOR", ssid=12, regex=False) def send(self, msg): """Send an APRS Message object.""" - payload = msg._filter_for_send() - frame = axframe.AX25UnnumberedInformationFrame( - msg.tocall, - msg.fromcall.encode("UTF-8"), - pid=0xF0, - repeaters=b"WIDE2-1", - payload=payload, + payload = f"{msg._filter_for_send()}" + self.aprsint.send_message( + addressee=msg.tocall, + message=payload, + path=["WIDE1-1", "WIDE2-1"], + oneshot=True, ) - LOG.debug(frame) - self.ax25int.transmit(frame) def get_client(): diff --git a/aprsd/main.py b/aprsd/main.py index 05099e7..8a8f269 100644 --- a/aprsd/main.py +++ b/aprsd/main.py @@ -469,6 +469,12 @@ def server( cl.client except LoginError: sys.exit(-1) + + rx_thread = threads.APRSDRXThread( + msg_queues=threads.msg_queues, + config=config, + ) + rx_thread.start() else: LOG.info( "APRS network connection Not Enabled in config. This is" @@ -486,13 +492,6 @@ def server( packets.PacketList(config=config) - rx_thread = threads.APRSDRXThread( - msg_queues=threads.msg_queues, - config=config, - ) - - rx_thread.start() - if "watch_list" in config["aprsd"] and config["aprsd"]["watch_list"].get( "enabled", True, diff --git a/aprsd/messaging.py b/aprsd/messaging.py index 8186813..5a3db6d 100644 --- a/aprsd/messaging.py +++ b/aprsd/messaging.py @@ -489,6 +489,9 @@ class AckMessage(Message): self.id, ) + def _filter_for_send(self): + return f"ack{self.id}" + def send(self): LOG.debug(f"Send ACK({self.tocall}:{self.id}) to radio.") thread = SendAckThread(self) diff --git a/aprsd/threads.py b/aprsd/threads.py index bd7a4c5..8592aa5 100644 --- a/aprsd/threads.py +++ b/aprsd/threads.py @@ -8,9 +8,7 @@ import tracemalloc import aprslib -from aprsd import ( - client, kissclient, messaging, packets, plugin, stats, trace, utils, -) +from aprsd import client, kissclient, messaging, packets, plugin, stats, utils LOG = logging.getLogger("APRSD") @@ -182,9 +180,10 @@ class APRSDRXThread(APRSDThread): class APRSDProcessPacketThread(APRSDThread): - def __init__(self, packet, config): + def __init__(self, packet, config, transport="aprsis"): self.packet = packet self.config = config + self.transport = transport name = self.packet["raw"][:10] super().__init__(f"RX_PACKET-{name}") @@ -239,6 +238,7 @@ class APRSDProcessPacketThread(APRSDThread): self.config["aprs"]["login"], fromcall, msg_id=msg_id, + transport=self.transport, ) ack.send() @@ -257,6 +257,7 @@ class APRSDProcessPacketThread(APRSDThread): self.config["aprs"]["login"], fromcall, subreply, + transport=self.transport, ) msg.send() @@ -273,6 +274,7 @@ class APRSDProcessPacketThread(APRSDThread): self.config["aprs"]["login"], fromcall, reply, + transport=self.transport, ) msg.send() @@ -285,6 +287,7 @@ class APRSDProcessPacketThread(APRSDThread): self.config["aprs"]["login"], fromcall, reply, + transport=self.transport, ) msg.send() except Exception as ex: @@ -296,6 +299,7 @@ class APRSDProcessPacketThread(APRSDThread): self.config["aprs"]["login"], fromcall, reply, + transport=self.transport, ) msg.send() @@ -349,7 +353,7 @@ class KISSRXThread(APRSDThread): # and the aprslib developer didn't want to allow a PR to add # kwargs. :( # https://github.com/rossengeorgiev/aprs-python/pull/56 - kiss_client.consumer(self.process_packet, callsign="APN382") + kiss_client.consumer(self.process_packet, callsign=self.config["kiss"]["callsign"]) kiss_client.loop.run_forever() except aprslib.exceptions.ConnectionDrop: @@ -361,131 +365,21 @@ class KISSRXThread(APRSDThread): client.Client().reset() # Continue to loop - @trace.trace - def process_packet(self, interface, frame, match): + def process_packet(self, interface, frame): """Process a packet recieved from aprs-is server.""" LOG.debug(f"Got an APRS Frame '{frame}'") - + # try and nuke the * from the fromcall sign. + frame.header._source._ch = False payload = str(frame.payload.decode()) msg = f"{str(frame.header)}:{payload}" + LOG.debug(f"Decoding {msg}") packet = aprslib.parse(msg) LOG.debug(packet) - - try: - stats.APRSDStats().msgs_rx_inc() - - msg = packet.get("message_text", None) - msg_format = packet.get("format", None) - msg_response = packet.get("response", None) - if msg_format == "message" and msg: - # we want to send the message through the - # plugins - self.process_message_packet(packet) - return - elif msg_response == "ack": - self.process_ack_packet(packet) - return - - if msg_format == "mic-e": - # process a mic-e packet - self.process_mic_e_packet(packet) - return - - except (aprslib.ParseError, aprslib.UnknownFormat) as exp: - LOG.exception("Failed to parse packet from aprs-is", exp) - - @trace.trace - def process_message_packet(self, packet): - LOG.debug("Message packet rx") - fromcall = packet["from"] - message = packet.get("message_text", None) - msg_id = packet.get("msgNo", "0") - messaging.log_message( - "Received Message", - packet["raw"], - message, - fromcall=fromcall, - msg_num=msg_id, - ) - found_command = False - # Get singleton of the PM - pm = plugin.PluginManager() - try: - results = pm.run(fromcall=fromcall, message=message, ack=msg_id) - for reply in results: - found_command = True - # A plugin can return a null message flag which signals - # us that they processed the message correctly, but have - # nothing to reply with, so we avoid replying with a usage string - if reply is not messaging.NULL_MESSAGE: - LOG.debug(f"Sending '{reply}'") - - msg = messaging.TextMessage( - self.config["aprs"]["login"], - fromcall, - reply, - transport=messaging.MESSAGE_TRANSPORT_TCPKISS, - ) - self.msg_queues["tx"].put(msg) - else: - LOG.debug("Got NULL MESSAGE from plugin") - - if not found_command: - plugins = pm.get_plugins() - names = [x.command_name for x in plugins] - names.sort() - - # reply = "Usage: {}".format(", ".join(names)) - reply = "Usage: weather, locate [call], time, fortune, ping" - - msg = messaging.TextMessage( - self.config["aprs"]["login"], - fromcall, - reply, - transport=messaging.MESSAGE_TRANSPORT_TCPKISS, - ) - self.msg_queues["tx"].put(msg) - except Exception as ex: - LOG.exception("Plugin failed!!!", ex) - reply = "A Plugin failed! try again?" - msg = messaging.TextMessage( - self.config["aprs"]["login"], - fromcall, - reply, - transport=messaging.MESSAGE_TRANSPORT_TCPKISS, - ) - self.msg_queues["tx"].put(msg) - - # let any threads do their thing, then ack - # send an ack last - ack = messaging.AckMessage( - self.config["aprs"]["login"], - fromcall, - msg_id=msg_id, + thread = APRSDProcessPacketThread( + packet=packet, config=self.config, transport=messaging.MESSAGE_TRANSPORT_TCPKISS, ) - self.msg_queues["tx"].put(ack) - LOG.debug("Packet processing complete") - - def process_ack_packet(self, packet): - ack_num = packet.get("msgNo") - LOG.info(f"Got ack for message {ack_num}") - messaging.log_message( - "ACK", - packet["raw"], - None, - ack=ack_num, - fromcall=packet["from"], - ) - tracker = messaging.MsgTrack() - tracker.remove(ack_num) - stats.APRSDStats().ack_rx_inc() - return - - def process_mic_e_packet(self, packet): - LOG.info("Mic-E Packet detected. Currenlty unsupported.") - messaging.log_packet(packet) - stats.APRSDStats().msgs_mice_inc() + thread.start() return diff --git a/requirements.in b/requirements.in index 7d96198..7703126 100644 --- a/requirements.in +++ b/requirements.in @@ -1,4 +1,4 @@ -aioax25 +aioax25>=0.0.10 aprslib click click-completion diff --git a/requirements.txt b/requirements.txt index c02df9e..f044a69 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,7 +4,7 @@ # # pip-compile requirements.in # -aioax25==0.0.9 +aioax25==0.0.10 # via -r requirements.in aprslib==0.6.47 # via -r requirements.in From ca438c9c60dd7152d9b3e5648177cce84988bd8b Mon Sep 17 00:00:00 2001 From: Hemna Date: Wed, 1 Sep 2021 15:17:57 -0400 Subject: [PATCH 4/4] Updated Admin UI to show KISS connections This updates the top area of the Admin UI to reflect the connection type (aprs-is vs kiss). --- aprsd/flask.py | 31 ++++++++++++++++++++++++++++++- aprsd/kissclient.py | 15 +++++++++++++++ aprsd/main.py | 7 +------ aprsd/messaging.py | 1 + aprsd/web/static/js/charts.js | 2 +- aprsd/web/templates/index.html | 2 +- 6 files changed, 49 insertions(+), 9 deletions(-) diff --git a/aprsd/flask.py b/aprsd/flask.py index 63014c9..e850899 100644 --- a/aprsd/flask.py +++ b/aprsd/flask.py @@ -11,7 +11,7 @@ from flask_httpauth import HTTPBasicAuth from werkzeug.security import check_password_hash, generate_password_hash import aprsd -from aprsd import messaging, packets, plugin, stats, utils +from aprsd import kissclient, messaging, packets, plugin, stats, utils LOG = logging.getLogger("APRSD") @@ -65,9 +65,38 @@ class APRSDFlask(flask_classful.FlaskView): plugins = pm.get_plugins() plugin_count = len(plugins) + if self.config["aprs"].get("enabled", True): + transport = "aprs-is" + aprs_connection = ( + "APRS-IS Server: " + "{}".format(stats["stats"]["aprs-is"]["server"]) + ) + else: + # We might be connected to a KISS socket? + if kissclient.KISSClient.kiss_enabled(self.config): + transport = kissclient.KISSClient.transport(self.config) + if transport == kissclient.TRANSPORT_TCPKISS: + aprs_connection = ( + "TCPKISS://{}:{}".format( + self.config["kiss"]["tcp"]["host"], + self.config["kiss"]["tcp"]["port"], + ) + ) + elif transport == kissclient.TRANSPORT_SERIALKISS: + aprs_connection = ( + "SerialKISS://{}@{} baud".format( + self.config["kiss"]["serial"]["device"], + self.config["kiss"]["serial"]["baudrate"], + ) + ) + + stats["transport"] = transport + stats["aprs_connection"] = aprs_connection + return flask.render_template( "index.html", initial_stats=stats, + aprs_connection=aprs_connection, callsign=self.config["aprs"]["login"], version=aprsd.__version__, config_json=json.dumps(self.config), diff --git a/aprsd/kissclient.py b/aprsd/kissclient.py index 3db801e..bed12c2 100644 --- a/aprsd/kissclient.py +++ b/aprsd/kissclient.py @@ -8,6 +8,8 @@ from aioax25.aprs import APRSInterface from aprsd import trace +TRANSPORT_TCPKISS = "tcpkiss" +TRANSPORT_SERIALKISS = "serialkiss" LOG = logging.getLogger("APRSD") @@ -32,6 +34,9 @@ class KISSClient: @staticmethod def kiss_enabled(config): """Return if tcp or serial KISS is enabled.""" + if "kiss" not in config: + return False + if "serial" in config["kiss"]: if config["kiss"]["serial"].get("enabled", False): return True @@ -40,6 +45,16 @@ class KISSClient: if config["kiss"]["tcp"].get("enabled", False): return True + @staticmethod + def transport(config): + if "serial" in config["kiss"]: + if config["kiss"]["serial"].get("enabled", False): + return TRANSPORT_SERIALKISS + + if "tcp" in config["kiss"]: + if config["kiss"]["tcp"].get("enabled", False): + return TRANSPORT_TCPKISS + @property def client(self): if not self.ax25client: diff --git a/aprsd/main.py b/aprsd/main.py index 8a8f269..bd9a711 100644 --- a/aprsd/main.py +++ b/aprsd/main.py @@ -491,12 +491,7 @@ def server( messaging.MsgTrack().load() packets.PacketList(config=config) - - if "watch_list" in config["aprsd"] and config["aprsd"]["watch_list"].get( - "enabled", - True, - ): - packets.WatchList(config=config) + packets.WatchList(config=config) if kissclient.KISSClient.kiss_enabled(config): kcl = kissclient.KISSClient(config=config) diff --git a/aprsd/messaging.py b/aprsd/messaging.py index 5a3db6d..f53ce0e 100644 --- a/aprsd/messaging.py +++ b/aprsd/messaging.py @@ -19,6 +19,7 @@ LOG = logging.getLogger("APRSD") NULL_MESSAGE = -1 MESSAGE_TRANSPORT_TCPKISS = "tcpkiss" +MESSAGE_TRANSPORT_SERIALKISS = "serialkiss" MESSAGE_TRANSPORT_APRSIS = "aprsis" diff --git a/aprsd/web/static/js/charts.js b/aprsd/web/static/js/charts.js index 9b42ffa..9d4ed65 100644 --- a/aprsd/web/static/js/charts.js +++ b/aprsd/web/static/js/charts.js @@ -220,7 +220,7 @@ function updateQuadData(chart, label, first, second, third, fourth) { function update_stats( data ) { $("#version").text( data["stats"]["aprsd"]["version"] ); - $("#aprsis").html( "APRS-IS Server: " + data["stats"]["aprs-is"]["server"] + "" ); + $("#aprs_connection").html( data["aprs_connection"] ); $("#uptime").text( "uptime: " + data["stats"]["aprsd"]["uptime"] ); const html_pretty = Prism.highlight(JSON.stringify(data, null, '\t'), Prism.languages.json, 'json'); $("#jsonstats").html(html_pretty); diff --git a/aprsd/web/templates/index.html b/aprsd/web/templates/index.html index 5314a1c..f82be37 100644 --- a/aprsd/web/templates/index.html +++ b/aprsd/web/templates/index.html @@ -58,7 +58,7 @@
{{ callsign }} connected to - NONE + {{ aprs_connection|safe }}