1
0
mirror of https://github.com/craigerl/aprsd.git synced 2024-11-25 17:38:44 -05:00

Started using dataclasses to describe packets

This patch adds new Packet classes to describe the
incoming packets parsed out from aprslib.
This commit is contained in:
Hemna 2022-12-14 19:21:25 -05:00
parent 2089b2575e
commit 082db7325d
8 changed files with 265 additions and 114 deletions

View File

@ -31,6 +31,7 @@ class Client:
connected = False connected = False
server_string = None server_string = None
filter = None
def __new__(cls, *args, **kwargs): def __new__(cls, *args, **kwargs):
"""This magic turns this into a singleton.""" """This magic turns this into a singleton."""
@ -44,10 +45,17 @@ class Client:
if config: if config:
self.config = config self.config = config
def set_filter(self, filter):
self.filter = filter
if self._client:
self._client.set_filter(filter)
@property @property
def client(self): def client(self):
if not self._client: if not self._client:
self._client = self.setup_connection() self._client = self.setup_connection()
if self.filter:
self._client.set_filter(self.filter)
return self._client return self._client
def reset(self): def reset(self):

View File

@ -5,10 +5,10 @@
# python included libs # python included libs
import datetime import datetime
import logging import logging
import signal
import sys import sys
import time import time
import aprslib
import click import click
from rich.console import Console from rich.console import Console
@ -16,7 +16,7 @@ from rich.console import Console
import aprsd import aprsd
from aprsd import cli_helper, client, messaging, packets, stats, threads, utils from aprsd import cli_helper, client, messaging, packets, stats, threads, utils
from aprsd.aprsd import cli from aprsd.aprsd import cli
from aprsd.utils import trace from aprsd.threads import rx
# setup the global logger # setup the global logger
@ -37,6 +37,14 @@ def signal_handler(sig, frame):
LOG.info(stats.APRSDStats()) LOG.info(stats.APRSDStats())
class APRSDListenThread(rx.APRSDRXThread):
def process_packet(self, *args, **kwargs):
raw = self._client.decode_packet(*args, **kwargs)
packet = packets.Packet.factory(raw)
LOG.debug(f"Got packet {packet}")
packet.log(header="RX Packet")
@cli.command() @cli.command()
@cli_helper.add_options(cli_helper.common_options) @cli_helper.add_options(cli_helper.common_options)
@click.option( @click.option(
@ -74,6 +82,8 @@ def listen(
o/obj1/obj2... - Object Filter Pass all objects with the exact name of obj1, obj2, ... (* wild card allowed)\n o/obj1/obj2... - Object Filter Pass all objects with the exact name of obj1, obj2, ... (* wild card allowed)\n
""" """
signal.signal(signal.SIGINT, signal_handler)
signal.signal(signal.SIGTERM, signal_handler)
config = ctx.obj["config"] config = ctx.obj["config"]
if not aprs_login: if not aprs_login:
@ -109,26 +119,6 @@ def listen(
packets.WatchList(config=config).load() packets.WatchList(config=config).load()
packets.SeenList(config=config).load() packets.SeenList(config=config).load()
@trace.trace
def rx_packet(packet):
resp = packet.get("response", None)
if resp == "ack":
ack_num = packet.get("msgNo")
console.log(f"We saw an ACK {ack_num} Ignoring")
messaging.log_packet(packet)
else:
message = packet.get("message_text", None)
fromcall = packet["from"]
msg_number = packet.get("msgNo", "0")
messaging.log_message(
"Received Message",
packet["raw"],
message,
fromcall=fromcall,
ack=msg_number,
console=console,
)
# Initialize the client factory and create # Initialize the client factory and create
# The correct client object ready for use # The correct client object ready for use
client.ClientFactory.setup(config) client.ClientFactory.setup(config)
@ -140,29 +130,21 @@ def listen(
# Creates the client object # Creates the client object
LOG.info("Creating client connection") LOG.info("Creating client connection")
aprs_client = client.factory.create() aprs_client = client.factory.create()
console.log(aprs_client) LOG.info(aprs_client)
LOG.debug(f"Filter by '{filter}'") LOG.debug(f"Filter by '{filter}'")
aprs_client.client.set_filter(filter) aprs_client.set_filter(filter)
packets.PacketList(config=config) packets.PacketList(config=config)
keepalive = threads.KeepAliveThread(config=config) keepalive = threads.KeepAliveThread(config=config)
keepalive.start() keepalive.start()
while True: LOG.debug("Create APRSDListenThread")
try: listen_thread = APRSDListenThread(threads.msg_queues, config=config)
# This will register a packet consumer with aprslib LOG.debug("Start APRSDListenThread")
# When new packets come in the consumer will process listen_thread.start()
# the packet LOG.debug("keepalive Join")
# with console.status("Listening for packets"): keepalive.join()
aprs_client.client.consumer(rx_packet, raw=False) LOG.debug("listen_thread Join")
except aprslib.exceptions.ConnectionDrop: listen_thread.join()
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
aprs_client.reset()
except aprslib.exceptions.UnknownFormat:
LOG.error("Got a Bad packet")

View File

@ -178,7 +178,7 @@ class WebChatProcessPacketThread(rx.APRSDProcessPacketThread):
) )
self.got_ack = True self.got_ack = True
def process_non_ack_packet(self, packet): def process_our_message_packet(self, packet):
LOG.info(f"process non ack PACKET {packet}") LOG.info(f"process non ack PACKET {packet}")
packet.get("addresse", None) packet.get("addresse", None)
fromcall = packet["from"] fromcall = packet["from"]

View File

@ -1,8 +1,10 @@
from dataclasses import asdict, dataclass, field
import datetime import datetime
import logging import logging
import threading import threading
import time import time
import dacite
import wrapt import wrapt
from aprsd import utils from aprsd import utils
@ -14,6 +16,150 @@ LOG = logging.getLogger("APRSD")
PACKET_TYPE_MESSAGE = "message" PACKET_TYPE_MESSAGE = "message"
PACKET_TYPE_ACK = "ack" PACKET_TYPE_ACK = "ack"
PACKET_TYPE_MICE = "mic-e" PACKET_TYPE_MICE = "mic-e"
PACKET_TYPE_WX = "weather"
PACKET_TYPE_UNKNOWN = "unknown"
PACKET_TYPE_STATUS = "status"
@dataclass
class Packet:
from_call: str
to_call: str
addresse: str = None
format: str = None
msgNo: str = None
packet_type: str = None
timestamp: float = field(default_factory=time.time)
raw: str = None
_raw_dict: dict = field(repr=True, default_factory=lambda: {})
@staticmethod
def factory(raw):
raw["_raw_dict"] = raw.copy()
translate_fields = {
"from": "from_call",
"to": "to_call",
}
# First translate some fields
for key in translate_fields:
if key in raw:
raw[translate_fields[key]] = raw[key]
del raw[key]
if "addresse" in raw:
raw["to_call"] = raw["addresse"]
class_lookup = {
PACKET_TYPE_WX: WeatherPacket,
PACKET_TYPE_MESSAGE: MessagePacket,
PACKET_TYPE_ACK: AckPacket,
PACKET_TYPE_MICE: MicEPacket,
PACKET_TYPE_STATUS: StatusPacket,
PACKET_TYPE_UNKNOWN: Packet,
}
packet_type = get_packet_type(raw)
raw["packet_type"] = packet_type
class_name = class_lookup[packet_type]
if packet_type == PACKET_TYPE_UNKNOWN:
# Try and figure it out here
if "latitude" in raw:
class_name = GPSPacket
if packet_type == PACKET_TYPE_WX:
# the weather information is in a dict
# this brings those values out to the outer dict
for key in raw["weather"]:
raw[key] = raw["weather"][key]
return dacite.from_dict(data_class=class_name, data=raw)
def log(self, header=None):
"""LOG a packet to the logfile."""
asdict(self)
log_list = ["\n"]
if header:
log_list.append(f"{header} _______________")
log_list.append(f" Packet : {self.__class__.__name__}")
log_list.append(f" Raw : {self.raw}")
if self.to_call:
log_list.append(f" To : {self.to_call}")
if self.from_call:
log_list.append(f" From : {self.from_call}")
if hasattr(self, "path"):
log_list.append(f" Path : {'=>'.join(self.path)}")
if hasattr(self, "via"):
log_list.append(f" VIA : {self.via}")
elif isinstance(self, MessagePacket):
log_list.append(f" Message : {self.message_text}")
if self.msgNo:
log_list.append(f" Msg # : {self.msgNo}")
log_list.append(f"{header} _______________ Complete")
LOG.info(self)
LOG.info("\n".join(log_list))
@dataclass
class PathPacket(Packet):
path: list[str] = field(default_factory=list)
via: str = None
@dataclass
class AckPacket(PathPacket):
response: str = None
@dataclass
class MessagePacket(PathPacket):
message_text: str = None
@dataclass
class StatusPacket(PathPacket):
status: str = None
timestamp: int = 0
messagecapable: bool = False
comment: str = None
@dataclass
class GPSPacket(PathPacket):
latitude: float = 0.00
longitude: float = 0.00
altitude: float = 0.00
rng: float = 0.00
posambiguity: int = 0
timestamp: int = 0
comment: str = None
symbol: str = None
symbol_table: str = None
speed: float = 0.00
course: int = 0
@dataclass
class MicEPacket(GPSPacket):
messagecapable: bool = False
mbits: str = None
mtype: str = None
@dataclass
class WeatherPacket(GPSPacket):
symbol: str = "_"
wind_gust: float = 0.00
temperature: float = 0.00
rain_1h: float = 0.00
rain_24h: float = 0.00
rain_since_midnight: float = 0.00
humidity: int = 0
pressure: float = 0.00
messagecapable: bool = False
comment: str = None
class PacketList: class PacketList:
@ -45,11 +191,8 @@ class PacketList:
@wrapt.synchronized(lock) @wrapt.synchronized(lock)
def add(self, packet): def add(self, packet):
packet["ts"] = time.time() packet.ts = time.time()
if ( if (packet.from_call == self.config["aprs"]["login"]):
"fromcall" in packet
and packet["fromcall"] == self.config["aprs"]["login"]
):
self.total_tx += 1 self.total_tx += 1
else: else:
self.total_recv += 1 self.total_recv += 1
@ -116,7 +259,10 @@ class WatchList(objectstore.ObjectStoreMixin):
@wrapt.synchronized(lock) @wrapt.synchronized(lock)
def update_seen(self, packet): def update_seen(self, packet):
callsign = packet["from"] if packet.addresse:
callsign = packet.addresse
else:
callsign = packet.from_call
if self.callsign_in_watchlist(callsign): if self.callsign_in_watchlist(callsign):
self.data[callsign]["last"] = datetime.datetime.now() self.data[callsign]["last"] = datetime.datetime.now()
self.data[callsign]["packets"].append(packet) self.data[callsign]["packets"].append(packet)
@ -178,10 +324,8 @@ class SeenList(objectstore.ObjectStoreMixin):
@wrapt.synchronized(lock) @wrapt.synchronized(lock)
def update_seen(self, packet): def update_seen(self, packet):
callsign = None callsign = None
if "fromcall" in packet: if packet.from_call:
callsign = packet["fromcall"] callsign = packet.from_call
elif "from" in packet:
callsign = packet["from"]
else: else:
LOG.warning(f"Can't find FROM in packet {packet}") LOG.warning(f"Can't find FROM in packet {packet}")
return return
@ -200,12 +344,16 @@ def get_packet_type(packet):
msg_format = packet.get("format", None) msg_format = packet.get("format", None)
msg_response = packet.get("response", None) msg_response = packet.get("response", None)
packet_type = "unknown" packet_type = "unknown"
if msg_format == "message": if msg_format == "message" and msg_response == "ack":
packet_type = PACKET_TYPE_MESSAGE
elif msg_response == "ack":
packet_type = PACKET_TYPE_ACK packet_type = PACKET_TYPE_ACK
elif msg_format == "message":
packet_type = PACKET_TYPE_MESSAGE
elif msg_format == "mic-e": elif msg_format == "mic-e":
packet_type = PACKET_TYPE_MICE packet_type = PACKET_TYPE_MICE
elif msg_format == "status":
packet_type = PACKET_TYPE_STATUS
elif packet.get("symbol", None) == "_":
packet_type = PACKET_TYPE_WX
return packet_type return packet_type

View File

@ -23,7 +23,6 @@ class APRSDRXThread(APRSDThread):
client.factory.create().client.stop() client.factory.create().client.stop()
def loop(self): def loop(self):
# setup the consumer of messages and block until a messages # setup the consumer of messages and block until a messages
try: try:
# This will register a packet consumer with aprslib # This will register a packet consumer with aprslib
@ -65,7 +64,12 @@ class APRSDPluginRXThread(APRSDRXThread):
processing in the PluginProcessPacketThread. processing in the PluginProcessPacketThread.
""" """
def process_packet(self, *args, **kwargs): def process_packet(self, *args, **kwargs):
packet = self._client.decode_packet(*args, **kwargs) raw = self._client.decode_packet(*args, **kwargs)
#LOG.debug(raw)
packet = packets.Packet.factory(raw.copy())
packet.log(header="RX Packet")
#LOG.debug(packet)
del raw
thread = APRSDPluginProcessPacketThread( thread = APRSDPluginProcessPacketThread(
config=self.config, config=self.config,
packet=packet, packet=packet,
@ -84,18 +88,18 @@ class APRSDProcessPacketThread(APRSDThread):
def __init__(self, config, packet): def __init__(self, config, packet):
self.config = config self.config = config
self.packet = packet self.packet = packet
name = self.packet["raw"][:10] name = self.packet.raw[:10]
super().__init__(f"RXPKT-{name}") super().__init__(f"RXPKT-{name}")
def process_ack_packet(self, packet): def process_ack_packet(self, packet):
ack_num = packet.get("msgNo") ack_num = packet.msgNo
LOG.info(f"Got ack for message {ack_num}") LOG.info(f"Got ack for message {ack_num}")
messaging.log_message( messaging.log_message(
"RXACK", "RXACK",
packet["raw"], packet.raw,
None, None,
ack=ack_num, ack=ack_num,
fromcall=packet["from"], fromcall=packet.from_call,
) )
tracker = messaging.MsgTrack() tracker = messaging.MsgTrack()
tracker.remove(ack_num) tracker.remove(ack_num)
@ -106,56 +110,60 @@ class APRSDProcessPacketThread(APRSDThread):
"""Process a packet received from aprs-is server.""" """Process a packet received from aprs-is server."""
packet = self.packet packet = self.packet
packets.PacketList().add(packet) packets.PacketList().add(packet)
our_call = self.config["aprsd"]["callsign"].lower()
fromcall = packet["from"] from_call = packet.from_call
tocall = packet.get("addresse", None) if packet.addresse:
msg = packet.get("message_text", None) to_call = packet.addresse
msg_id = packet.get("msgNo", "0") else:
msg_response = packet.get("response", None) to_call = packet.to_call
# LOG.debug(f"Got packet from '{fromcall}' - {packet}") msg_id = packet.msgNo
# We don't put ack packets destined for us through the # We don't put ack packets destined for us through the
# plugins. # plugins.
wl = packets.WatchList()
wl.update_seen(packet)
if ( if (
tocall isinstance(packet, packets.AckPacket)
and tocall.lower() == self.config["aprsd"]["callsign"].lower() and packet.addresse.lower() == our_call
and msg_response == "ack"
): ):
self.process_ack_packet(packet) self.process_ack_packet(packet)
else: else:
# It's not an ACK for us, so lets run it through
# the plugins.
messaging.log_message(
"Received Message",
packet["raw"],
msg,
fromcall=fromcall,
msg_num=msg_id,
)
# Only ack messages that were sent directly to us # Only ack messages that were sent directly to us
if ( if isinstance(packet, packets.MessagePacket):
tocall if to_call and to_call.lower() == our_call:
and tocall.lower() == self.config["aprsd"]["callsign"].lower() # It's a MessagePacket and it's for us!
): stats.APRSDStats().msgs_rx_inc()
stats.APRSDStats().msgs_rx_inc() # let any threads do their thing, then ack
# let any threads do their thing, then ack # send an ack last
# send an ack last ack = messaging.AckMessage(
ack = messaging.AckMessage( self.config["aprsd"]["callsign"],
self.config["aprsd"]["callsign"], from_call,
fromcall, msg_id=msg_id,
msg_id=msg_id, )
) ack.send()
ack.send()
self.process_non_ack_packet(packet) self.process_our_message_packet(packet)
else:
# Packet wasn't meant for us!
self.process_other_packet(packet, for_us=False)
else: else:
LOG.info("Packet was not for us.") self.process_other_packet(
packet, for_us=(to_call.lower() == our_call),
)
LOG.debug("Packet processing complete") LOG.debug("Packet processing complete")
@abc.abstractmethod @abc.abstractmethod
def process_non_ack_packet(self, *args, **kwargs): def process_our_message_packet(self, *args, **kwargs):
"""Ack packets already dealt with here.""" """Process a MessagePacket destined for us!"""
def process_other_packet(self, packet, for_us=False):
"""Process an APRS Packet that isn't a message or ack"""
if not for_us:
LOG.info("Got a packet not meant for us.")
else:
LOG.info("Got a non AckPacket/MessagePacket")
LOG.info(packet)
class APRSDPluginProcessPacketThread(APRSDProcessPacketThread): class APRSDPluginProcessPacketThread(APRSDProcessPacketThread):
@ -163,18 +171,19 @@ class APRSDPluginProcessPacketThread(APRSDProcessPacketThread):
This is the main aprsd server plugin processing thread.""" This is the main aprsd server plugin processing thread."""
def process_non_ack_packet(self, packet): def process_our_message_packet(self, packet):
"""Send the packet through the plugins.""" """Send the packet through the plugins."""
fromcall = packet["from"] from_call = packet.from_call
tocall = packet.get("addresse", None) if packet.addresse:
msg = packet.get("message_text", None) to_call = packet.addresse
packet.get("msgNo", "0") else:
packet.get("response", None) to_call = None
# msg = packet.get("message_text", None)
# packet.get("msgNo", "0")
# packet.get("response", None)
pm = plugin.PluginManager() pm = plugin.PluginManager()
try: try:
results = pm.run(packet) results = pm.run(packet)
wl = packets.WatchList()
wl.update_seen(packet)
replied = False replied = False
for reply in results: for reply in results:
if isinstance(reply, list): if isinstance(reply, list):
@ -187,7 +196,7 @@ class APRSDPluginProcessPacketThread(APRSDProcessPacketThread):
else: else:
msg = messaging.TextMessage( msg = messaging.TextMessage(
self.config["aprsd"]["callsign"], self.config["aprsd"]["callsign"],
fromcall, from_call,
subreply, subreply,
) )
msg.send() msg.send()
@ -207,18 +216,18 @@ class APRSDPluginProcessPacketThread(APRSDProcessPacketThread):
msg = messaging.TextMessage( msg = messaging.TextMessage(
self.config["aprsd"]["callsign"], self.config["aprsd"]["callsign"],
fromcall, from_call,
reply, reply,
) )
msg.send() msg.send()
# If the message was for us and we didn't have a # If the message was for us and we didn't have a
# response, then we send a usage statement. # response, then we send a usage statement.
if tocall == self.config["aprsd"]["callsign"] and not replied: if to_call == self.config["aprsd"]["callsign"] and not replied:
LOG.warning("Sending help!") LOG.warning("Sending help!")
msg = messaging.TextMessage( msg = messaging.TextMessage(
self.config["aprsd"]["callsign"], self.config["aprsd"]["callsign"],
fromcall, from_call,
"Unknown command! Send 'help' message for help", "Unknown command! Send 'help' message for help",
) )
msg.send() msg.send()
@ -226,11 +235,11 @@ class APRSDPluginProcessPacketThread(APRSDProcessPacketThread):
LOG.error("Plugin failed!!!") LOG.error("Plugin failed!!!")
LOG.exception(ex) LOG.exception(ex)
# Do we need to send a reply? # Do we need to send a reply?
if tocall == self.config["aprsd"]["callsign"]: if to_call == self.config["aprsd"]["callsign"]:
reply = "A Plugin failed! try again?" reply = "A Plugin failed! try again?"
msg = messaging.TextMessage( msg = messaging.TextMessage(
self.config["aprsd"]["callsign"], self.config["aprsd"]["callsign"],
fromcall, from_call,
reply, reply,
) )
msg.send() msg.send()

View File

@ -4,7 +4,7 @@
# #
# pip-compile --annotation-style=line --resolver=backtracking dev-requirements.in # pip-compile --annotation-style=line --resolver=backtracking dev-requirements.in
# #
add-trailing-comma==2.3.0 # via gray add-trailing-comma==2.4.0 # via gray
alabaster==0.7.12 # via sphinx alabaster==0.7.12 # via sphinx
attrs==22.1.0 # via jsonschema, pytest attrs==22.1.0 # via jsonschema, pytest
autoflake==1.5.3 # via gray autoflake==1.5.3 # via gray
@ -34,7 +34,7 @@ imagesize==1.4.1 # via sphinx
importlib-metadata==5.1.0 # via sphinx importlib-metadata==5.1.0 # via sphinx
importlib-resources==5.10.1 # via fixit importlib-resources==5.10.1 # via fixit
iniconfig==1.1.1 # via pytest iniconfig==1.1.1 # via pytest
isort==5.10.1 # via -r dev-requirements.in, gray isort==5.11.2 # via -r dev-requirements.in, gray
jinja2==3.1.2 # via sphinx jinja2==3.1.2 # via sphinx
jsonschema==4.17.3 # via fixit jsonschema==4.17.3 # via fixit
libcst==0.4.9 # via fixit libcst==0.4.9 # via fixit
@ -47,7 +47,7 @@ packaging==22.0 # via build, pyproject-api, pytest, sphinx, tox
pathspec==0.10.3 # via black pathspec==0.10.3 # via black
pep517==0.13.0 # via build pep517==0.13.0 # via build
pep8-naming==0.13.2 # via -r dev-requirements.in pep8-naming==0.13.2 # via -r dev-requirements.in
pip-tools==6.11.0 # via -r dev-requirements.in pip-tools==6.12.0 # via -r dev-requirements.in
platformdirs==2.6.0 # via black, tox, virtualenv platformdirs==2.6.0 # via black, tox, virtualenv
pluggy==1.0.0 # via pytest, tox pluggy==1.0.0 # via pytest, tox
pre-commit==2.20.0 # via -r dev-requirements.in pre-commit==2.20.0 # via -r dev-requirements.in
@ -74,7 +74,7 @@ sphinxcontrib-serializinghtml==1.1.5 # via sphinx
tokenize-rt==5.0.0 # via add-trailing-comma, pyupgrade tokenize-rt==5.0.0 # via add-trailing-comma, pyupgrade
toml==0.10.2 # via autoflake, pre-commit toml==0.10.2 # via autoflake, pre-commit
tomli==2.0.1 # via black, build, coverage, mypy, pep517, pyproject-api, pytest, tox tomli==2.0.1 # via black, build, coverage, mypy, pep517, pyproject-api, pytest, tox
tox==4.0.8 # via -r dev-requirements.in tox==4.0.9 # via -r dev-requirements.in
typing-extensions==4.4.0 # via black, libcst, mypy, typing-inspect typing-extensions==4.4.0 # via black, libcst, mypy, typing-inspect
typing-inspect==0.8.0 # via libcst typing-inspect==0.8.0 # via libcst
unify==0.5 # via gray unify==0.5 # via gray

View File

@ -27,3 +27,5 @@ attrs==22.1.0
# for mobile checking # for mobile checking
user-agents user-agents
pyopenssl pyopenssl
dataclasses
dacite2

View File

@ -17,6 +17,8 @@ click==8.1.3 # via -r requirements.in, click-completion, flask
click-completion==0.5.2 # via -r requirements.in click-completion==0.5.2 # via -r requirements.in
commonmark==0.9.1 # via rich commonmark==0.9.1 # via rich
cryptography==38.0.4 # via pyopenssl cryptography==38.0.4 # via pyopenssl
dacite2==2.0.0 # via -r requirements.in
dataclasses==0.6 # via -r requirements.in
dnspython==2.2.1 # via eventlet dnspython==2.2.1 # via eventlet
eventlet==0.33.2 # via -r requirements.in eventlet==0.33.2 # via -r requirements.in
flask==2.1.2 # via -r requirements.in, flask-classful, flask-httpauth, flask-socketio flask==2.1.2 # via -r requirements.in, flask-classful, flask-httpauth, flask-socketio