Merge pull request #154 from craigerl/packet_updates

Packet updates
This commit is contained in:
Walter A. Boring IV 2024-03-25 09:20:35 -04:00 committed by GitHub
commit 144ad34ae5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
28 changed files with 964 additions and 633 deletions

View File

@ -137,7 +137,7 @@ class APRSISClient(Client):
def decode_packet(self, *args, **kwargs):
"""APRS lib already decodes this."""
return core.Packet.factory(args[0])
return core.factory(args[0])
def setup_connection(self):
user = CONF.aprs_network.login
@ -238,7 +238,7 @@ class KISSClient(Client):
# LOG.debug(f"Decoding {msg}")
raw = aprslib.parse(str(frame))
packet = core.Packet.factory(raw)
packet = core.factory(raw)
if isinstance(packet, core.ThirdParty):
return packet.subpacket
else:

View File

@ -67,7 +67,7 @@ class APRSDFakeClient(metaclass=trace.TraceWrapperMetaclass):
# Generate packets here?
raw = "GTOWN>APDW16,WIDE1-1,WIDE2-1:}KM6LYW-9>APZ100,TCPIP,GTOWN*::KM6LYW :KM6LYW: 19 Miles SW"
pkt_raw = aprslib.parse(raw)
pkt = core.Packet.factory(pkt_raw)
pkt = core.factory(pkt_raw)
callback(packet=pkt)
LOG.debug(f"END blocking FAKE consumer {self}")
time.sleep(8)

View File

@ -17,6 +17,7 @@ from rich.console import Console
import aprsd
from aprsd import cli_helper, client, packets, plugin, stats, threads
from aprsd.main import cli
from aprsd.packets import log as packet_log
from aprsd.rpc import server as rpc_server
from aprsd.threads import rx
@ -53,27 +54,31 @@ class APRSDListenThread(rx.APRSDRXThread):
filters = {
packets.Packet.__name__: packets.Packet,
packets.AckPacket.__name__: packets.AckPacket,
packets.BeaconPacket.__name__: packets.BeaconPacket,
packets.GPSPacket.__name__: packets.GPSPacket,
packets.MessagePacket.__name__: packets.MessagePacket,
packets.MicEPacket.__name__: packets.MicEPacket,
packets.ObjectPacket.__name__: packets.ObjectPacket,
packets.StatusPacket.__name__: packets.StatusPacket,
packets.ThirdPartyPacket.__name__: packets.ThirdPartyPacket,
packets.WeatherPacket.__name__: packets.WeatherPacket,
packets.UnknownPacket.__name__: packets.UnknownPacket,
}
if self.packet_filter:
filter_class = filters[self.packet_filter]
if isinstance(packet, filter_class):
packet.log(header="RX")
packet_log.log(packet)
if self.plugin_manager:
# Don't do anything with the reply
# This is the listen only command.
self.plugin_manager.run(packet)
else:
packet_log.log(packet)
if self.plugin_manager:
# Don't do anything with the reply.
# This is the listen only command.
self.plugin_manager.run(packet)
else:
packet.log(header="RX")
packets.PacketList().rx(packet)
@ -96,11 +101,16 @@ class APRSDListenThread(rx.APRSDRXThread):
"--packet-filter",
type=click.Choice(
[
packets.Packet.__name__,
packets.AckPacket.__name__,
packets.BeaconPacket.__name__,
packets.GPSPacket.__name__,
packets.MicEPacket.__name__,
packets.MessagePacket.__name__,
packets.ObjectPacket.__name__,
packets.RejectPacket.__name__,
packets.StatusPacket.__name__,
packets.ThirdPartyPacket.__name__,
packets.UnknownPacket.__name__,
packets.WeatherPacket.__name__,
],
case_sensitive=False,
@ -180,7 +190,7 @@ def listen(
aprs_client.set_filter(filter)
keepalive = threads.KeepAliveThread()
keepalive.start()
# keepalive.start()
if CONF.rpc_settings.enabled:
rpc = rpc_server.APRSDRPCThread()
@ -205,6 +215,8 @@ def listen(
)
LOG.debug("Start APRSDListenThread")
listen_thread.start()
keepalive.start()
LOG.debug("keepalive Join")
keepalive.join()
LOG.debug("listen_thread Join")

View File

@ -7,7 +7,6 @@ import sys
import threading
import time
from aprslib import util as aprslib_util
import click
import flask
from flask import request
@ -22,7 +21,6 @@ import aprsd
from aprsd import (
cli_helper, client, packets, plugin_utils, stats, threads, utils,
)
from aprsd.log import log
from aprsd.main import cli
from aprsd.threads import aprsd as aprsd_threads
from aprsd.threads import rx, tx
@ -30,7 +28,7 @@ from aprsd.utils import trace
CONF = cfg.CONF
LOG = logging.getLogger("APRSD")
LOG = logging.getLogger()
auth = HTTPBasicAuth()
users = {}
socketio = None
@ -335,7 +333,6 @@ class WebChatProcessPacketThread(rx.APRSDProcessPacketThread):
def process_our_message_packet(self, packet: packets.MessagePacket):
global callsign_locations
LOG.info(f"process MessagePacket {repr(packet)}")
# ok lets see if we have the location for the
# person we just sent a message to.
from_call = packet.get("from_call").upper()
@ -541,10 +538,10 @@ class SendMessageNamespace(Namespace):
def on_gps(self, data):
LOG.debug(f"WS on_GPS: {data}")
lat = aprslib_util.latitude_to_ddm(data["latitude"])
long = aprslib_util.longitude_to_ddm(data["longitude"])
LOG.debug(f"Lat DDM {lat}")
LOG.debug(f"Long DDM {long}")
lat = data["latitude"]
long = data["longitude"]
LOG.debug(f"Lat {lat}")
LOG.debug(f"Long {long}")
tx.send(
packets.GPSPacket(
@ -572,8 +569,6 @@ class SendMessageNamespace(Namespace):
def init_flask(loglevel, quiet):
global socketio, flask_app
log.setup_logging(loglevel, quiet)
socketio = SocketIO(
flask_app, logger=False, engineio_logger=False,
async_mode="threading",
@ -624,7 +619,7 @@ def webchat(ctx, flush, port):
LOG.info(msg)
LOG.info(f"APRSD Started version: {aprsd.__version__}")
CONF.log_opt_values(LOG, logging.DEBUG)
CONF.log_opt_values(logging.getLogger(), logging.DEBUG)
user = CONF.admin.user
users[user] = generate_password_hash(CONF.admin.password)
if not port:

View File

@ -101,6 +101,14 @@ aprsd_opts = [
default=None,
help="Longitude for the GPS Beacon button. If not set, the button will not be enabled.",
),
cfg.StrOpt(
"log_packet_format",
choices=["compact", "multiline", "both"],
default="compact",
help="When logging packets 'compact' will use a single line formatted for each packet."
"'multiline' will use multiple lines for each packet and is the traditional format."
"both will log both compact and multiline.",
),
]
watch_list_opts = [
@ -225,6 +233,11 @@ webchat_opts = [
default=None,
help="Longitude for the GPS Beacon button. If not set, the button will not be enabled.",
),
cfg.BoolOpt(
"disable_url_request_logging",
default=False,
help="Disable the logging of url requests in the webchat command.",
),
]
registry_opts = [

View File

@ -10,7 +10,8 @@ from aprsd.conf import log as conf_log
CONF = cfg.CONF
LOG = logging.getLogger("APRSD")
# LOG = logging.getLogger("APRSD")
LOG = logger
logging_queue = queue.Queue()
@ -35,6 +36,7 @@ class InterceptHandler(logging.Handler):
# to disable log to stdout, but still log to file
# use the --quiet option on the cmdln
def setup_logging(loglevel=None, quiet=False):
print(f"setup_logging: loglevel={loglevel}, quiet={quiet}")
if not loglevel:
log_level = CONF.logging.log_level
else:
@ -53,9 +55,13 @@ def setup_logging(loglevel=None, quiet=False):
"aprslib.parsing",
"aprslib.exceptions",
]
webserver_list = [
"werkzeug",
"werkzeug._internal",
]
# We don't really want to see the aprslib parsing debug output.
disable_list = imap_list + aprslib_list
disable_list = imap_list + aprslib_list + webserver_list
# remove every other logger's handlers
# and propagate to root logger
@ -66,17 +72,29 @@ def setup_logging(loglevel=None, quiet=False):
else:
logging.getLogger(name).propagate = True
if CONF.webchat.disable_url_request_logging:
for name in webserver_list:
logging.getLogger(name).handlers = []
logging.getLogger(name).propagate = True
logging.getLogger(name).setLevel(logging.ERROR)
handlers = [
{
"sink": sys.stdout, "serialize": False,
"sink": sys.stdout,
"serialize": False,
"format": CONF.logging.logformat,
"colorize": True,
"level": log_level,
},
]
if CONF.logging.logfile:
handlers.append(
{
"sink": CONF.logging.logfile, "serialize": False,
"sink": CONF.logging.logfile,
"serialize": False,
"format": CONF.logging.logformat,
"colorize": False,
"level": log_level,
},
)
@ -90,8 +108,11 @@ def setup_logging(loglevel=None, quiet=False):
{
"sink": qh, "serialize": False,
"format": CONF.logging.logformat,
"level": log_level,
"colorize": False,
},
)
# configure loguru
logger.configure(handlers=handlers)
logger.level("DEBUG", color="<fg #BABABA>")

View File

@ -1,6 +1,7 @@
from aprsd.packets.core import ( # noqa: F401
AckPacket, BeaconPacket, GPSPacket, MessagePacket, MicEPacket, Packet,
RejectPacket, StatusPacket, WeatherPacket,
AckPacket, BeaconPacket, BulletinPacket, GPSPacket, MessagePacket,
MicEPacket, ObjectPacket, Packet, RejectPacket, StatusPacket,
ThirdPartyPacket, UnknownPacket, WeatherPacket, factory,
)
from aprsd.packets.packet_list import PacketList # noqa: F401
from aprsd.packets.seen_list import SeenList # noqa: F401

File diff suppressed because it is too large Load Diff

128
aprsd/packets/log.py Normal file
View File

@ -0,0 +1,128 @@
import logging
from typing import Optional
from loguru import logger
from oslo_config import cfg
from aprsd.packets.core import AckPacket, RejectPacket
LOG = logging.getLogger()
LOGU = logger
CONF = cfg.CONF
FROM_COLOR = "fg #C70039"
TO_COLOR = "fg #D033FF"
TX_COLOR = "red"
RX_COLOR = "green"
PACKET_COLOR = "cyan"
def log_multiline(packet, tx: Optional[bool] = False, header: Optional[bool] = True) -> None:
"""LOG a packet to the logfile."""
if CONF.log_packet_format == "compact":
return
# asdict(packet)
logit = ["\n"]
name = packet.__class__.__name__
if header:
if tx:
header_str = f"<{TX_COLOR}>TX</{TX_COLOR}>"
logit.append(
f"{header_str}________(<{PACKET_COLOR}>{name}</{PACKET_COLOR}> "
f"TX:{packet.send_count + 1} of {packet.retry_count})",
)
else:
header_str = f"<{RX_COLOR}>RX</{RX_COLOR}>"
logit.append(
f"{header_str}________(<{PACKET_COLOR}>{name}</{PACKET_COLOR}>)",
)
else:
header_str = ""
logit.append(f"__________(<{PACKET_COLOR}>{name}</{PACKET_COLOR}>)")
# log_list.append(f" Packet : {packet.__class__.__name__}")
if packet.msgNo:
logit.append(f" Msg # : {packet.msgNo}")
if packet.from_call:
logit.append(f" From : <{FROM_COLOR}>{packet.from_call}</{FROM_COLOR}>")
if packet.to_call:
logit.append(f" To : <{TO_COLOR}>{packet.to_call}</{TO_COLOR}>")
if hasattr(packet, "path") and packet.path:
logit.append(f" Path : {'=>'.join(packet.path)}")
if hasattr(packet, "via") and packet.via:
logit.append(f" VIA : {packet.via}")
if not isinstance(packet, AckPacket) and not isinstance(packet, RejectPacket):
msg = packet.human_info
if msg:
msg = msg.replace("<", "\\<")
logit.append(f" Info : <light-yellow><b>{msg}</b></light-yellow>")
if hasattr(packet, "comment") and packet.comment:
logit.append(f" Comment : {packet.comment}")
raw = packet.raw.replace("<", "\\<")
logit.append(f" Raw : <fg #828282>{raw}</fg #828282>")
logit.append(f"{header_str}________(<{PACKET_COLOR}>{name}</{PACKET_COLOR}>)")
LOGU.opt(colors=True).info("\n".join(logit))
LOG.debug(repr(packet))
def log(packet, tx: Optional[bool] = False, header: Optional[bool] = True) -> None:
if CONF.log_packet_format == "multiline":
log_multiline(packet, tx, header)
return
logit = []
name = packet.__class__.__name__
if header:
if tx:
via_color = "red"
arrow = f"<{via_color}>-></{via_color}>"
logit.append(
f"<red>TX {arrow}</red> "
f"<cyan>{name}</cyan>"
f":{packet.msgNo}"
f" ({packet.send_count + 1} of {packet.retry_count})",
)
else:
via_color = "fg #828282"
arrow = f"<{via_color}>-></{via_color}>"
left_arrow = f"<{via_color}><-</{via_color}>"
logit.append(
f"<fg #1AA730>RX</fg #1AA730> {left_arrow} "
f"<cyan>{name}</cyan>"
f":{packet.msgNo}",
)
else:
via_color = "green"
arrow = f"<{via_color}>-></{via_color}>"
logit.append(
f"<cyan>{name}</cyan>"
f":{packet.msgNo}",
)
tmp = None
if packet.path:
tmp = f"{arrow}".join(packet.path) + f"{arrow} "
logit.append(
f"<{FROM_COLOR}>{packet.from_call}</{FROM_COLOR}> {arrow}"
f"{tmp if tmp else ' '}"
f"<{TO_COLOR}>{packet.to_call}</{TO_COLOR}>",
)
if not isinstance(packet, AckPacket) and not isinstance(packet, RejectPacket):
logit.append(":")
msg = packet.human_info
if msg:
msg = msg.replace("<", "\\<")
logit.append(f"<light-yellow><b>{msg}</b></light-yellow>")
LOGU.opt(colors=True).info(" ".join(logit))
log_multiline(packet, tx, header)

View File

@ -1,4 +1,5 @@
# The base plugin class
from __future__ import annotations
import abc
import importlib
import inspect
@ -42,7 +43,7 @@ class APRSDPluginSpec:
"""A hook specification namespace."""
@hookspec
def filter(self, packet: packets.core.Packet):
def filter(self, packet: type[packets.Packet]):
"""My special little hook that you can customize."""
@ -65,7 +66,7 @@ class APRSDPluginBase(metaclass=abc.ABCMeta):
self.threads = self.create_threads() or []
self.start_threads()
def start_threads(self):
def start_threads(self) -> None:
if self.enabled and self.threads:
if not isinstance(self.threads, list):
self.threads = [self.threads]
@ -90,10 +91,10 @@ class APRSDPluginBase(metaclass=abc.ABCMeta):
)
@property
def message_count(self):
def message_count(self) -> int:
return self.message_counter
def help(self):
def help(self) -> str:
return "Help!"
@abc.abstractmethod
@ -118,11 +119,11 @@ class APRSDPluginBase(metaclass=abc.ABCMeta):
thread.stop()
@abc.abstractmethod
def filter(self, packet: packets.core.Packet):
def filter(self, packet: type[packets.Packet]) -> str | packets.MessagePacket:
pass
@abc.abstractmethod
def process(self, packet: packets.core.Packet):
def process(self, packet: type[packets.Packet]):
"""This is called when the filter passes."""
@ -154,7 +155,7 @@ class APRSDWatchListPluginBase(APRSDPluginBase, metaclass=abc.ABCMeta):
LOG.warning("Watch list enabled, but no callsigns set.")
@hookimpl
def filter(self, packet: packets.core.Packet):
def filter(self, packet: type[packets.Packet]) -> str | packets.MessagePacket:
result = packets.NULL_MESSAGE
if self.enabled:
wl = watch_list.WatchList()
@ -206,14 +207,14 @@ class APRSDRegexCommandPluginBase(APRSDPluginBase, metaclass=abc.ABCMeta):
self.enabled = True
@hookimpl
def filter(self, packet: packets.core.MessagePacket):
LOG.info(f"{self.__class__.__name__} called")
def filter(self, packet: packets.MessagePacket) -> str | packets.MessagePacket:
LOG.debug(f"{self.__class__.__name__} called")
if not self.enabled:
result = f"{self.__class__.__name__} isn't enabled"
LOG.warning(result)
return result
if not isinstance(packet, packets.core.MessagePacket):
if not isinstance(packet, packets.MessagePacket):
LOG.warning(f"{self.__class__.__name__} Got a {packet.__class__.__name__} ignoring")
return packets.NULL_MESSAGE
@ -226,7 +227,7 @@ class APRSDRegexCommandPluginBase(APRSDPluginBase, metaclass=abc.ABCMeta):
# and is an APRS message format and has a message.
if (
tocall == CONF.callsign
and isinstance(packet, packets.core.MessagePacket)
and isinstance(packet, packets.MessagePacket)
and message
):
if re.search(self.command_regex, message, re.IGNORECASE):
@ -269,7 +270,7 @@ class HelpPlugin(APRSDRegexCommandPluginBase):
def help(self):
return "Help: send APRS help or help <plugin>"
def process(self, packet: packets.core.MessagePacket):
def process(self, packet: packets.MessagePacket):
LOG.info("HelpPlugin")
# fromcall = packet.get("from")
message = packet.message_text
@ -469,12 +470,12 @@ class PluginManager:
LOG.info("Completed Plugin Loading.")
def run(self, packet: packets.core.MessagePacket):
def run(self, packet: packets.MessagePacket):
"""Execute all the plugins run method."""
with self.lock:
return self._pluggy_pm.hook.filter(packet=packet)
def run_watchlist(self, packet: packets.core.Packet):
def run_watchlist(self, packet: packets.Packet):
with self.lock:
return self._watchlist_pm.hook.filter(packet=packet)

View File

@ -23,9 +23,7 @@ class VersionPlugin(plugin.APRSDRegexCommandPluginBase):
# fromcall = packet.get("from")
# message = packet.get("message_text", None)
# ack = packet.get("msgNo", "0")
stats_obj = stats.APRSDStats()
s = stats_obj.stats()
print(s)
s = stats.APRSDStats().stats()
return "APRSD ver:{} uptime:{}".format(
aprsd.__version__,
s["aprsd"]["uptime"],

View File

@ -110,7 +110,6 @@ class USMetarPlugin(plugin.APRSDRegexCommandPluginBase, plugin.APRSFIKEYMixin):
@trace.trace
def process(self, packet):
print("FISTY")
fromcall = packet.get("from")
message = packet.get("message_text", None)
# ack = packet.get("msgNo", "0")

View File

@ -174,7 +174,6 @@ class APRSDStats:
def email_thread_update(self):
self._email_thread_last_time = datetime.datetime.now()
@wrapt.synchronized(lock)
def stats(self):
now = datetime.datetime.now()
if self._email_thread_last_time:

View File

@ -2,6 +2,7 @@ import abc
import datetime
import logging
import threading
from typing import List
import wrapt
@ -9,42 +10,6 @@ import wrapt
LOG = logging.getLogger("APRSD")
class APRSDThreadList:
"""Singleton class that keeps track of application wide threads."""
_instance = None
threads_list = []
lock = threading.Lock()
def __new__(cls, *args, **kwargs):
if cls._instance is None:
cls._instance = super().__new__(cls)
cls.threads_list = []
return cls._instance
@wrapt.synchronized(lock)
def add(self, thread_obj):
self.threads_list.append(thread_obj)
@wrapt.synchronized(lock)
def remove(self, thread_obj):
self.threads_list.remove(thread_obj)
@wrapt.synchronized(lock)
def stop_all(self):
"""Iterate over all threads and call stop on them."""
for th in self.threads_list:
LOG.info(f"Stopping Thread {th.name}")
if hasattr(th, "packet"):
LOG.info(F"{th.name} packet {th.packet}")
th.stop()
@wrapt.synchronized(lock)
def __len__(self):
return len(self.threads_list)
class APRSDThread(threading.Thread, metaclass=abc.ABCMeta):
def __init__(self, name):
@ -86,3 +51,39 @@ class APRSDThread(threading.Thread, metaclass=abc.ABCMeta):
self._cleanup()
APRSDThreadList().remove(self)
LOG.debug("Exiting")
class APRSDThreadList:
"""Singleton class that keeps track of application wide threads."""
_instance = None
threads_list: List[APRSDThread] = []
lock = threading.Lock()
def __new__(cls, *args, **kwargs):
if cls._instance is None:
cls._instance = super().__new__(cls)
cls.threads_list = []
return cls._instance
@wrapt.synchronized(lock)
def add(self, thread_obj):
self.threads_list.append(thread_obj)
@wrapt.synchronized(lock)
def remove(self, thread_obj):
self.threads_list.remove(thread_obj)
@wrapt.synchronized(lock)
def stop_all(self):
"""Iterate over all threads and call stop on them."""
for th in self.threads_list:
LOG.info(f"Stopping Thread {th.name}")
if hasattr(th, "packet"):
LOG.info(F"{th.name} packet {th.packet}")
th.stop()
@wrapt.synchronized(lock)
def __len__(self):
return len(self.threads_list)

View File

@ -7,6 +7,7 @@ import aprslib
from oslo_config import cfg
from aprsd import client, packets, plugin
from aprsd.packets import log as packet_log
from aprsd.threads import APRSDThread, tx
@ -80,7 +81,7 @@ class APRSDDupeRXThread(APRSDRXThread):
"""
packet = self._client.decode_packet(*args, **kwargs)
# LOG.debug(raw)
packet.log(header="RX")
packet_log.log(packet)
if isinstance(packet, packets.AckPacket):
# We don't need to drop AckPackets, those should be
@ -142,14 +143,14 @@ class APRSDProcessPacketThread(APRSDThread):
def process_ack_packet(self, packet):
"""We got an ack for a message, no need to resend it."""
ack_num = packet.msgNo
LOG.info(f"Got ack for message {ack_num}")
LOG.debug(f"Got ack for message {ack_num}")
pkt_tracker = packets.PacketTrack()
pkt_tracker.remove(ack_num)
def process_reject_packet(self, packet):
"""We got a reject message for a packet. Stop sending the message."""
ack_num = packet.msgNo
LOG.info(f"Got REJECT for message {ack_num}")
LOG.debug(f"Got REJECT for message {ack_num}")
pkt_tracker = packets.PacketTrack()
pkt_tracker.remove(ack_num)

View File

@ -10,7 +10,9 @@ from rush.stores import dictionary
from aprsd import client
from aprsd import conf # noqa
from aprsd import threads as aprsd_threads
from aprsd.packets import core, tracker
from aprsd.packets import core
from aprsd.packets import log as packet_log
from aprsd.packets import tracker
CONF = cfg.CONF
@ -74,7 +76,7 @@ def _send_direct(packet, aprs_client=None):
cl = client.factory.create()
packet.update_timestamp()
packet.log(header="TX")
packet_log.log(packet, tx=True)
cl.send(packet)
@ -163,7 +165,7 @@ class SendAckThread(aprsd_threads.APRSDThread):
if self.packet.send_count == self.packet.retry_count:
# we reached the send limit, don't send again
# TODO(hemna) - Need to put this in a delayed queue?
LOG.info(
LOG.debug(
f"{self.packet.__class__.__name__}"
f"({self.packet.msgNo}) "
"Send Complete. Max attempts reached"

View File

@ -1,9 +1,13 @@
from multiprocessing import RawValue
import random
import threading
import wrapt
MAX_PACKET_ID = 9999
class PacketCounter:
"""
Global Packet id counter class.
@ -17,19 +21,18 @@ class PacketCounter:
"""
_instance = None
max_count = 9999
lock = threading.Lock()
def __new__(cls, *args, **kwargs):
"""Make this a singleton class."""
if cls._instance is None:
cls._instance = super().__new__(cls, *args, **kwargs)
cls._instance.val = RawValue("i", 1)
cls._instance.val = RawValue("i", random.randint(1, MAX_PACKET_ID))
return cls._instance
@wrapt.synchronized(lock)
def increment(self):
if self.val.value == self.max_count:
if self.val.value == MAX_PACKET_ID:
self.val.value = 1
else:
self.val.value += 1

View File

@ -37,7 +37,7 @@ function start_update() {
update_stats(data);
},
complete: function() {
setTimeout(statsworker, 10000);
setTimeout(statsworker, 60000);
}
});
})();

View File

@ -22,7 +22,7 @@ CONF = cfg.CONF
LOG = logging.getLogger("gunicorn.access")
auth = HTTPBasicAuth()
users = {}
users: dict[str, str] = {}
app = Flask(
"aprsd",
static_url_path="/static",

View File

@ -4,209 +4,80 @@
#
# pip-compile --annotation-style=line dev-requirements.in
#
add-trailing-comma==3.1.0
# via gray
alabaster==0.7.16
# via sphinx
autoflake==1.5.3
# via gray
babel==2.14.0
# via sphinx
black==24.3.0
# via gray
build==1.1.1
# via pip-tools
cachetools==5.3.3
# via tox
certifi==2024.2.2
# via requests
cfgv==3.4.0
# via pre-commit
chardet==5.2.0
# via tox
charset-normalizer==3.3.2
# via requests
click==8.1.7
# via
# black
# fixit
# moreorless
# pip-tools
colorama==0.4.6
# via tox
commonmark==0.9.1
# via rich
configargparse==1.7
# via gray
coverage[toml]==7.4.3
# via pytest-cov
distlib==0.3.8
# via virtualenv
docutils==0.20.1
# via sphinx
exceptiongroup==1.2.0
# via pytest
filelock==3.13.1
# via
# tox
# virtualenv
fixit==2.1.0
# via gray
flake8==7.0.0
# via
# -r dev-requirements.in
# pep8-naming
gray==0.14.0
# via -r dev-requirements.in
identify==2.5.35
# via pre-commit
idna==3.6
# via requests
imagesize==1.4.1
# via sphinx
iniconfig==2.0.0
# via pytest
isort==5.13.2
# via
# -r dev-requirements.in
# gray
jinja2==3.1.3
# via sphinx
libcst==1.2.0
# via fixit
markupsafe==2.1.5
# via jinja2
mccabe==0.7.0
# via flake8
moreorless==0.4.0
# via fixit
mypy==1.8.0
# via -r dev-requirements.in
mypy-extensions==1.0.0
# via
# black
# mypy
# typing-inspect
nodeenv==1.8.0
# via pre-commit
packaging==23.2
# via
# black
# build
# fixit
# pyproject-api
# pytest
# sphinx
# tox
pathspec==0.12.1
# via
# black
# trailrunner
pep8-naming==0.13.3
# via -r dev-requirements.in
pip-tools==7.4.1
# via -r dev-requirements.in
platformdirs==4.2.0
# via
# black
# tox
# virtualenv
pluggy==1.4.0
# via
# pytest
# tox
pre-commit==3.6.2
# via -r dev-requirements.in
pycodestyle==2.11.1
# via flake8
pyflakes==3.2.0
# via
# autoflake
# flake8
pygments==2.17.2
# via
# rich
# sphinx
pyproject-api==1.6.1
# via tox
pyproject-hooks==1.0.0
# via
# build
# pip-tools
pytest==8.0.2
# via
# -r dev-requirements.in
# pytest-cov
pytest-cov==4.1.0
# via -r dev-requirements.in
pyupgrade==3.15.1
# via gray
pyyaml==6.0.1
# via
# libcst
# pre-commit
requests==2.31.0
# via sphinx
rich==12.6.0
# via gray
snowballstemmer==2.2.0
# via sphinx
sphinx==7.2.6
# via -r dev-requirements.in
sphinxcontrib-applehelp==1.0.8
# via sphinx
sphinxcontrib-devhelp==1.0.6
# via sphinx
sphinxcontrib-htmlhelp==2.0.5
# via sphinx
sphinxcontrib-jsmath==1.0.1
# via sphinx
sphinxcontrib-qthelp==1.0.7
# via sphinx
sphinxcontrib-serializinghtml==1.1.10
# via sphinx
tokenize-rt==5.2.0
# via
# add-trailing-comma
# pyupgrade
toml==0.10.2
# via autoflake
tomli==2.0.1
# via
# black
# build
# coverage
# fixit
# mypy
# pip-tools
# pyproject-api
# pyproject-hooks
# pytest
# tox
tox==4.14.0
# via -r dev-requirements.in
trailrunner==1.4.0
# via fixit
typing-extensions==4.10.0
# via
# black
# libcst
# mypy
# typing-inspect
typing-inspect==0.9.0
# via libcst
unify==0.5
# via gray
untokenize==0.1.1
# via unify
urllib3==2.2.1
# via requests
virtualenv==20.25.1
# via
# pre-commit
# tox
wheel==0.42.0
# via pip-tools
add-trailing-comma==3.1.0 # via gray
alabaster==0.7.16 # via sphinx
autoflake==1.5.3 # via gray
babel==2.14.0 # via sphinx
black==24.3.0 # via gray
build==1.1.1 # via pip-tools
cachetools==5.3.3 # via tox
certifi==2024.2.2 # via requests
cfgv==3.4.0 # via pre-commit
chardet==5.2.0 # via tox
charset-normalizer==3.3.2 # via requests
click==8.1.7 # via black, fixit, moreorless, pip-tools
colorama==0.4.6 # via tox
commonmark==0.9.1 # via rich
configargparse==1.7 # via gray
coverage[toml]==7.4.4 # via pytest-cov
distlib==0.3.8 # via virtualenv
docutils==0.20.1 # via sphinx
exceptiongroup==1.2.0 # via pytest
filelock==3.13.1 # via tox, virtualenv
fixit==2.1.0 # via gray
flake8==7.0.0 # via -r dev-requirements.in, pep8-naming
gray==0.14.0 # via -r dev-requirements.in
identify==2.5.35 # via pre-commit
idna==3.6 # via requests
imagesize==1.4.1 # via sphinx
iniconfig==2.0.0 # via pytest
isort==5.13.2 # via -r dev-requirements.in, gray
jinja2==3.1.3 # via sphinx
libcst==1.2.0 # via fixit
markupsafe==2.1.5 # via jinja2
mccabe==0.7.0 # via flake8
moreorless==0.4.0 # via fixit
mypy==1.9.0 # via -r dev-requirements.in
mypy-extensions==1.0.0 # via black, mypy, typing-inspect
nodeenv==1.8.0 # via pre-commit
packaging==24.0 # via black, build, fixit, pyproject-api, pytest, sphinx, tox
pathspec==0.12.1 # via black, trailrunner
pep8-naming==0.13.3 # via -r dev-requirements.in
pip-tools==7.4.1 # via -r dev-requirements.in
platformdirs==4.2.0 # via black, tox, virtualenv
pluggy==1.4.0 # via pytest, tox
pre-commit==3.6.2 # via -r dev-requirements.in
pycodestyle==2.11.1 # via flake8
pyflakes==3.2.0 # via autoflake, flake8
pygments==2.17.2 # via rich, sphinx
pyproject-api==1.6.1 # via tox
pyproject-hooks==1.0.0 # via build, pip-tools
pytest==8.1.1 # via -r dev-requirements.in, pytest-cov
pytest-cov==4.1.0 # via -r dev-requirements.in
pyupgrade==3.15.1 # via gray
pyyaml==6.0.1 # via libcst, pre-commit
requests==2.31.0 # via sphinx
rich==12.6.0 # via gray
snowballstemmer==2.2.0 # via sphinx
sphinx==7.2.6 # via -r dev-requirements.in
sphinxcontrib-applehelp==1.0.8 # via sphinx
sphinxcontrib-devhelp==1.0.6 # via sphinx
sphinxcontrib-htmlhelp==2.0.5 # via sphinx
sphinxcontrib-jsmath==1.0.1 # via sphinx
sphinxcontrib-qthelp==1.0.7 # via sphinx
sphinxcontrib-serializinghtml==1.1.10 # via sphinx
tokenize-rt==5.2.0 # via add-trailing-comma, pyupgrade
toml==0.10.2 # via autoflake
tomli==2.0.1 # via black, build, coverage, fixit, mypy, pip-tools, pyproject-api, pyproject-hooks, pytest, tox
tox==4.14.1 # via -r dev-requirements.in
trailrunner==1.4.0 # via fixit
typing-extensions==4.10.0 # via black, libcst, mypy, typing-inspect
typing-inspect==0.9.0 # via libcst
unify==0.5 # via gray
untokenize==0.1.1 # via unify
urllib3==2.2.1 # via requests
virtualenv==20.25.1 # via pre-commit, tox
wheel==0.43.0 # via pip-tools
# The following packages are considered to be unsafe in a requirements file:
# pip

View File

@ -28,7 +28,6 @@ wrapt
kiss3
attrs
dataclasses
dacite2
oslo.config
rpyc>=6.0.0
# Pin this here so it doesn't require a compile on

View File

@ -17,7 +17,6 @@ click==8.1.7 # via -r requirements.in, click-completion, click-para
click-completion==0.5.2 # via -r requirements.in
click-params==0.5.0 # via -r requirements.in
commonmark==0.9.1 # via rich
dacite2==2.0.0 # via -r requirements.in
dataclasses==0.6 # via -r requirements.in
dataclasses-json==0.6.4 # via -r requirements.in
debtcollector==3.0.0 # via oslo-config
@ -34,7 +33,7 @@ greenlet==3.0.3 # via eventlet, gevent
h11==0.14.0 # via wsproto
idna==3.6 # via requests
imapclient==3.0.1 # via -r requirements.in
importlib-metadata==7.0.1 # via ax253, kiss3
importlib-metadata==7.0.2 # via ax253, kiss3
itsdangerous==2.1.2 # via flask
jinja2==3.1.3 # via click-completion, flask
kiss3==8.0.0 # via -r requirements.in
@ -45,7 +44,7 @@ mypy-extensions==1.0.0 # via typing-inspect
netaddr==1.2.1 # via oslo-config
oslo-config==9.4.0 # via -r requirements.in
oslo-i18n==6.3.0 # via oslo-config
packaging==23.2 # via marshmallow
packaging==24.0 # via marshmallow
pbr==6.0.0 # via -r requirements.in, oslo-i18n, stevedore
pluggy==1.4.0 # via -r requirements.in
plumbum==1.8.2 # via rpyc
@ -76,7 +75,7 @@ validators==0.22.0 # via click-params
werkzeug==3.0.1 # via -r requirements.in, flask
wrapt==1.16.0 # via -r requirements.in, debtcollector, deprecated
wsproto==1.2.0 # via simple-websocket
zipp==3.17.0 # via importlib-metadata
zipp==3.18.1 # via importlib-metadata
zope-event==5.0 # via gevent
zope-interface==6.2 # via gevent

View File

@ -51,11 +51,8 @@ class TestSendMessageCommand(unittest.TestCase):
):
self.config_and_init()
mock_socketio.emit = mock.MagicMock()
packet = fake.fake_packet(
message="blah",
msg_number=1,
message_format=core.PACKET_TYPE_ACK,
)
# Create an ACK packet
packet = fake.fake_ack_packet()
mock_queue = mock.MagicMock()
socketio = mock.MagicMock()
wcp = webchat.WebChatProcessPacketThread(mock_queue, socketio)

View File

@ -1,4 +1,4 @@
from aprsd import packets, plugin, threads
from aprsd import plugin, threads
from aprsd.packets import core
@ -13,6 +13,7 @@ def fake_packet(
message=None,
msg_number=None,
message_format=core.PACKET_TYPE_MESSAGE,
response=None,
):
packet_dict = {
"from": fromcall,
@ -27,7 +28,17 @@ def fake_packet(
if msg_number:
packet_dict["msgNo"] = str(msg_number)
return packets.Packet.factory(packet_dict)
if response:
packet_dict["response"] = response
return core.factory(packet_dict)
def fake_ack_packet():
return fake_packet(
msg_number=12,
response=core.PACKET_TYPE_ACK,
)
class FakeBaseNoThreadsPlugin(plugin.APRSDPluginBase):

View File

@ -11,7 +11,7 @@ from .. import fake, test_plugin
CONF = cfg.CONF
class TestUSWeatherPluginPlugin(test_plugin.TestPlugin):
class TestUSWeatherPlugin(test_plugin.TestPlugin):
def test_not_enabled_missing_aprs_fi_key(self):
# When the aprs.fi api key isn't set, then

View File

@ -1,13 +1,16 @@
import unittest
from unittest import mock
import aprslib
from aprslib import util as aprslib_util
from aprsd import packets
from aprsd.packets import core
from . import fake
class TestPluginBase(unittest.TestCase):
class TestPacketBase(unittest.TestCase):
def _fake_dict(
self,
@ -55,7 +58,7 @@ class TestPluginBase(unittest.TestCase):
def test_packet_factory(self):
pkt_dict = self._fake_dict()
pkt = packets.Packet.factory(pkt_dict)
pkt = packets.factory(pkt_dict)
self.assertIsInstance(pkt, packets.MessagePacket)
self.assertEqual(fake.FAKE_FROM_CALLSIGN, pkt.from_call)
@ -71,7 +74,7 @@ class TestPluginBase(unittest.TestCase):
"comment": "Home!",
}
pkt_dict["format"] = core.PACKET_TYPE_UNCOMPRESSED
pkt = packets.Packet.factory(pkt_dict)
pkt = packets.factory(pkt_dict)
self.assertIsInstance(pkt, packets.WeatherPacket)
@mock.patch("aprsd.packets.core.GPSPacket._build_time_zulu")
@ -100,3 +103,183 @@ class TestPluginBase(unittest.TestCase):
wx.prepare()
expected = "KFAKE>KMINE,WIDE1-1,WIDE2-1:@221450z0.0/0.0_000/000g000t000r001p000P000h00b00000"
self.assertEqual(expected, wx.raw)
def test_beacon_factory(self):
"""Test to ensure a beacon packet is created."""
packet_raw = "WB4BOR-12>APZ100,WIDE2-1:@161647z3724.15N107847.58W$ APRSD WebChat"
packet_dict = aprslib.parse(packet_raw)
packet = packets.factory(packet_dict)
self.assertIsInstance(packet, packets.BeaconPacket)
packet_raw = "kd8mey-10>APRS,TCPIP*,qAC,T2SYDNEY:=4247.80N/08539.00WrPHG1210/Making 220 Great Again Allstar# 552191"
packet_dict = aprslib.parse(packet_raw)
packet = packets.factory(packet_dict)
self.assertIsInstance(packet, packets.BeaconPacket)
def test_reject_factory(self):
"""Test to ensure a reject packet is created."""
packet_raw = "HB9FDL-1>APK102,HB9FM-4*,WIDE2,qAR,HB9FEF-11::REPEAT :rej4139"
packet_dict = aprslib.parse(packet_raw)
packet = packets.factory(packet_dict)
self.assertIsInstance(packet, packets.RejectPacket)
self.assertEqual("4139", packet.msgNo)
self.assertEqual("HB9FDL-1", packet.from_call)
self.assertEqual("REPEAT", packet.to_call)
self.assertEqual("reject", packet.packet_type)
self.assertIsNone(packet.payload)
def test_thirdparty_factory(self):
"""Test to ensure a third party packet is created."""
packet_raw = "GTOWN>APDW16,WIDE1-1,WIDE2-1:}KM6LYW-9>APZ100,TCPIP,GTOWN*::KM6LYW :KM6LYW: 19 Miles SW"
packet_dict = aprslib.parse(packet_raw)
packet = packets.factory(packet_dict)
self.assertIsInstance(packet, packets.ThirdPartyPacket)
def test_weather_factory(self):
"""Test to ensure a weather packet is created."""
packet_raw = "FW9222>APRS,TCPXX*,qAX,CWOP-6:@122025z2953.94N/08423.77W_232/003g006t084r000p032P000h80b10157L745.DsWLL"
packet_dict = aprslib.parse(packet_raw)
packet = packets.factory(packet_dict)
self.assertIsInstance(packet, packets.WeatherPacket)
self.assertEqual(28.88888888888889, packet.temperature)
self.assertEqual(0.0, packet.rain_1h)
self.assertEqual(1015.7, packet.pressure)
self.assertEqual(80, packet.humidity)
self.assertEqual(745, packet.luminosity)
self.assertEqual(3.0, packet.wind_speed)
self.assertEqual(232, packet.wind_direction)
self.assertEqual(6.0, packet.wind_gust)
self.assertEqual(29.899, packet.latitude)
self.assertEqual(-84.39616666666667, packet.longitude)
def test_mice_factory(self):
packet_raw = 'kh2sr-15>S7TSYR,WIDE1-1,WIDE2-1,qAO,KO6KL-1:`1`7\x1c\x1c.#/`"4,}QuirkyQRP 4.6V 35.3C S06'
packet_dict = aprslib.parse(packet_raw)
packet = packets.factory(packet_dict)
self.assertIsInstance(packet, packets.MicEPacket)
# Packet with telemetry and DAO
# http://www.aprs.org/datum.txt
packet_raw = 'KD9YIL>T0PX9W,WIDE1-1,WIDE2-1,qAO,NU9R-10:`sB,l#P>/\'"6+}|#*%U\'a|!whl!|3'
packet_dict = aprslib.parse(packet_raw)
packet = packets.factory(packet_dict)
self.assertIsInstance(packet, packets.MicEPacket)
def test_ack_format(self):
"""Test the ack packet format."""
ack = packets.AckPacket(
from_call=fake.FAKE_FROM_CALLSIGN,
to_call=fake.FAKE_TO_CALLSIGN,
msgNo=123,
)
expected = f"{fake.FAKE_FROM_CALLSIGN}>APZ100::{fake.FAKE_TO_CALLSIGN:<9}:ack123"
self.assertEqual(expected, str(ack))
def test_reject_format(self):
"""Test the reject packet format."""
reject = packets.RejectPacket(
from_call=fake.FAKE_FROM_CALLSIGN,
to_call=fake.FAKE_TO_CALLSIGN,
msgNo=123,
)
expected = f"{fake.FAKE_FROM_CALLSIGN}>APZ100::{fake.FAKE_TO_CALLSIGN:<9}:rej123"
self.assertEqual(expected, str(reject))
def test_beacon_format(self):
"""Test the beacon packet format."""
lat = 28.123456
lon = -80.123456
ts = 1711219496.6426
comment = "My Beacon Comment"
packet = packets.BeaconPacket(
from_call=fake.FAKE_FROM_CALLSIGN,
to_call=fake.FAKE_TO_CALLSIGN,
latitude=lat,
longitude=lon,
timestamp=ts,
symbol=">",
comment=comment,
)
expected_lat = aprslib_util.latitude_to_ddm(lat)
expected_lon = aprslib_util.longitude_to_ddm(lon)
expected = f"KFAKE>APZ100:@231844z{expected_lat}/{expected_lon}>{comment}"
self.assertEqual(expected, str(packet))
def test_beacon_format_no_comment(self):
"""Test the beacon packet format."""
lat = 28.123456
lon = -80.123456
ts = 1711219496.6426
packet = packets.BeaconPacket(
from_call=fake.FAKE_FROM_CALLSIGN,
to_call=fake.FAKE_TO_CALLSIGN,
latitude=lat,
longitude=lon,
timestamp=ts,
symbol=">",
)
empty_comment = "APRSD Beacon"
expected_lat = aprslib_util.latitude_to_ddm(lat)
expected_lon = aprslib_util.longitude_to_ddm(lon)
expected = f"KFAKE>APZ100:@231844z{expected_lat}/{expected_lon}>{empty_comment}"
self.assertEqual(expected, str(packet))
def test_bulletin_format(self):
"""Test the bulletin packet format."""
# bulletin id = 0
bid = 0
packet = packets.BulletinPacket(
from_call=fake.FAKE_FROM_CALLSIGN,
message_text="My Bulletin Message",
bid=0,
)
expected = f"{fake.FAKE_FROM_CALLSIGN}>APZ100::BLN{bid:<9}:{packet.message_text}"
self.assertEqual(expected, str(packet))
# bulletin id = 1
bid = 1
txt = "((((((( CX2SA - Salto Uruguay ))))))) http://www.cx2sa.org"
packet = packets.BulletinPacket(
from_call=fake.FAKE_FROM_CALLSIGN,
message_text=txt,
bid=1,
)
expected = f"{fake.FAKE_FROM_CALLSIGN}>APZ100::BLN{bid:<9}:{txt}"
self.assertEqual(expected, str(packet))
def test_message_format(self):
"""Test the message packet format."""
message = "My Message"
msgno = "ABX"
packet = packets.MessagePacket(
from_call=fake.FAKE_FROM_CALLSIGN,
to_call=fake.FAKE_TO_CALLSIGN,
message_text=message,
msgNo=msgno,
)
expected = f"{fake.FAKE_FROM_CALLSIGN}>APZ100::{fake.FAKE_TO_CALLSIGN:<9}:{message}{{{msgno}"
self.assertEqual(expected, str(packet))
# test with bad words
# Currently fails with mixed case
message = "My cunt piss fuck shIt text"
exp_msg = "My **** **** **** **** text"
msgno = "ABX"
packet = packets.MessagePacket(
from_call=fake.FAKE_FROM_CALLSIGN,
to_call=fake.FAKE_TO_CALLSIGN,
message_text=message,
msgNo=msgno,
)
expected = f"{fake.FAKE_FROM_CALLSIGN}>APZ100::{fake.FAKE_TO_CALLSIGN:<9}:{exp_msg}{{{msgno}"
self.assertEqual(expected, str(packet))

View File

@ -45,7 +45,6 @@ class TestPluginManager(unittest.TestCase):
self.assertEqual([], plugin_list)
pm.setup_plugins()
plugin_list = pm.get_plugins()
print(plugin_list)
self.assertIsInstance(plugin_list, list)
self.assertIsInstance(
plugin_list[0],
@ -163,9 +162,7 @@ class TestPluginBase(TestPlugin):
self.assertEqual(expected, actual)
mock_process.assert_not_called()
packet = fake.fake_packet(
message_format=core.PACKET_TYPE_ACK,
)
packet = fake.fake_ack_packet()
expected = packets.NULL_MESSAGE
actual = p.filter(packet)
self.assertEqual(expected, actual)

View File

@ -94,7 +94,7 @@ skip_install = true
deps = -r{toxinidir}/requirements.txt
-r{toxinidir}/dev-requirements.txt
commands =
mypy aprsd
mypy --ignore-missing-imports --install-types aprsd
[testenv:pre-commit]
skip_install = true