1
0
mirror of https://github.com/craigerl/aprsd.git synced 2025-08-19 05:02:27 -04:00

Merge pull request #91 from craigerl/small_refactor

Small refactor
This commit is contained in:
Walter A. Boring IV 2022-11-23 13:33:23 -05:00 committed by GitHub
commit e66dc344b8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
82 changed files with 2090 additions and 874 deletions

View File

@ -7,7 +7,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy: strategy:
matrix: matrix:
python-version: [3.7, 3.8, 3.9] python-version: ["3.8", "3.9", "3.10"]
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- name: Set up Python ${{ matrix.python-version }} - name: Set up Python ${{ matrix.python-version }}

View File

@ -25,7 +25,7 @@ docs: build
cp Changelog docs/changelog.rst cp Changelog docs/changelog.rst
tox -edocs tox -edocs
clean: clean-build clean-pyc clean-test ## remove all build, test, coverage and Python artifacts clean: clean-build clean-pyc clean-test clean-dev ## remove all build, test, coverage and Python artifacts
clean-build: ## remove build artifacts clean-build: ## remove build artifacts
rm -fr build/ rm -fr build/
@ -46,6 +46,9 @@ clean-test: ## remove test and coverage artifacts
rm -fr htmlcov/ rm -fr htmlcov/
rm -fr .pytest_cache rm -fr .pytest_cache
clean-dev:
rm -rf $(VENVDIR)
test: dev ## Run all the tox tests test: dev ## Run all the tox tests
tox -p all tox -p all
@ -73,5 +76,9 @@ docker-dev: test ## Make a development docker container tagged with hemna6969/a
docker build -t hemna6969/aprsd:master -f docker/Dockerfile-dev docker docker build -t hemna6969/aprsd:master -f docker/Dockerfile-dev docker
update-requirements: dev ## Update the requirements.txt and dev-requirements.txt files update-requirements: dev ## Update the requirements.txt and dev-requirements.txt files
rm requirements.txt
rm dev-requirements.txt
touch requirements.txt
touch dev-requirements.txt
$(VENV)/pip-compile requirements.in $(VENV)/pip-compile requirements.in
$(VENV)/pip-compile dev-requirements.in $(VENV)/pip-compile dev-requirements.in

View File

@ -68,9 +68,9 @@ def main():
# The commands themselves live in the cmds directory # The commands themselves live in the cmds directory
from .cmds import ( # noqa from .cmds import ( # noqa
completion, dev, healthcheck, list_plugins, listen, send_message, completion, dev, healthcheck, list_plugins, listen, send_message,
server, server, webchat,
) )
cli() cli(auto_envvar_prefix="APRSD")
def signal_handler(sig, frame): def signal_handler(sig, frame):

View File

@ -4,7 +4,8 @@ import typing as t
import click import click
from aprsd import config as aprsd_config from aprsd import config as aprsd_config
from aprsd import log from aprsd.logging import log
from aprsd.utils import trace
F = t.TypeVar("F", bound=t.Callable[..., t.Any]) F = t.TypeVar("F", bound=t.Callable[..., t.Any])
@ -59,6 +60,8 @@ def process_standard_options(f: F) -> F:
ctx.obj["loglevel"], ctx.obj["loglevel"],
ctx.obj["quiet"], ctx.obj["quiet"],
) )
if ctx.obj["config"]["aprsd"].get("trace", False):
trace.setup_tracing(["method", "api"])
del kwargs["loglevel"] del kwargs["loglevel"]
del kwargs["config_file"] del kwargs["config_file"]

View File

@ -6,8 +6,9 @@ import aprslib
from aprslib.exceptions import LoginError from aprslib.exceptions import LoginError
from aprsd import config as aprsd_config from aprsd import config as aprsd_config
from aprsd import exception, trace from aprsd import exception
from aprsd.clients import aprsis, kiss from aprsd.clients import aprsis, kiss
from aprsd.utils import trace
LOG = logging.getLogger("APRSD") LOG = logging.getLogger("APRSD")
@ -51,7 +52,8 @@ class Client:
def reset(self): def reset(self):
"""Call this to force a rebuild/reconnect.""" """Call this to force a rebuild/reconnect."""
del self._client if self._client:
del self._client
@abc.abstractmethod @abc.abstractmethod
def setup_connection(self): def setup_connection(self):
@ -129,6 +131,7 @@ class APRSISClient(Client):
backoff = backoff * 2 backoff = backoff * 2
continue continue
LOG.debug(f"Logging in to APRS-IS with user '{user}'") LOG.debug(f"Logging in to APRS-IS with user '{user}'")
self._client = aprs_client
return aprs_client return aprs_client
@ -153,8 +156,8 @@ class KISSClient(Client):
# Ensure that the config vars are correctly set # Ensure that the config vars are correctly set
if KISSClient.is_enabled(config): if KISSClient.is_enabled(config):
config.check_option( config.check_option(
"kiss.callsign", "aprsd.callsign",
default_fail=aprsd_config.DEFAULT_CONFIG_DICT["kiss"]["callsign"], default_fail=aprsd_config.DEFAULT_CONFIG_DICT["aprsd"]["callsign"],
) )
transport = KISSClient.transport(config) transport = KISSClient.transport(config)
if transport == TRANSPORT_SERIALKISS: if transport == TRANSPORT_SERIALKISS:
@ -192,8 +195,8 @@ class KISSClient(Client):
@trace.trace @trace.trace
def setup_connection(self): def setup_connection(self):
ax25client = kiss.Aioax25Client(self.config) client = kiss.KISS3Client(self.config)
return ax25client return client
class ClientFactory: class ClientFactory:
@ -220,7 +223,7 @@ class ClientFactory:
elif KISSClient.is_enabled(self.config): elif KISSClient.is_enabled(self.config):
key = KISSClient.transport(self.config) key = KISSClient.transport(self.config)
LOG.debug(f"GET client {key}") LOG.debug(f"GET client '{key}'")
builder = self._builders.get(key) builder = self._builders.get(key)
if not builder: if not builder:
raise ValueError(key) raise ValueError(key)

View File

@ -1,5 +1,7 @@
import logging import logging
import select import select
import socket
import threading
import aprslib import aprslib
from aprslib import is_py3 from aprslib import is_py3
@ -7,6 +9,7 @@ from aprslib.exceptions import (
ConnectionDrop, ConnectionError, GenericError, LoginError, ParseError, ConnectionDrop, ConnectionError, GenericError, LoginError, ParseError,
UnknownFormat, UnknownFormat,
) )
import wrapt
import aprsd import aprsd
from aprsd import stats from aprsd import stats
@ -23,11 +26,30 @@ class Aprsdis(aprslib.IS):
# timeout in seconds # timeout in seconds
select_timeout = 1 select_timeout = 1
lock = threading.Lock()
def stop(self): def stop(self):
self.thread_stop = True self.thread_stop = True
LOG.info("Shutdown Aprsdis client.") LOG.info("Shutdown Aprsdis client.")
def is_socket_closed(self, sock: socket.socket) -> bool:
try:
# this will try to read bytes without blocking and also without removing them from buffer (peek only)
data = sock.recv(16, socket.MSG_DONTWAIT | socket.MSG_PEEK)
if len(data) == 0:
return True
except BlockingIOError:
return False # socket is open and reading from it would block
except ConnectionResetError:
return True # socket was closed for some other reason
except Exception:
self.logger.exception(
"unexpected exception when checking if a socket is closed",
)
return False
return False
@wrapt.synchronized(lock)
def send(self, msg): def send(self, msg):
"""Send an APRS Message object.""" """Send an APRS Message object."""
line = str(msg) line = str(msg)

View File

@ -1,20 +1,19 @@
import asyncio
import logging import logging
from aioax25 import interface import aprslib
from aioax25 import kiss as kiss from ax253 import Frame
from aioax25.aprs import APRSInterface import kiss
from aprsd import messaging
from aprsd.utils import trace
LOG = logging.getLogger("APRSD") LOG = logging.getLogger("APRSD")
class Aioax25Client: class KISS3Client:
def __init__(self, config): def __init__(self, config):
self.config = config self.config = config
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
self.loop = asyncio.get_event_loop()
self.setup() self.setup()
def setup(self): def setup(self):
@ -24,71 +23,89 @@ class Aioax25Client:
False, False,
): ):
LOG.debug( LOG.debug(
"Setting up Serial KISS connection to {}".format( "KISS({}) Serial connection to {}".format(
kiss.__version__,
self.config["kiss"]["serial"]["device"], self.config["kiss"]["serial"]["device"],
), ),
) )
self.kissdev = kiss.SerialKISSDevice( self.kiss = kiss.SerialKISS(
device=self.config["kiss"]["serial"]["device"], port=self.config["kiss"]["serial"]["device"],
baudrate=self.config["kiss"]["serial"].get("baudrate", 9600), speed=self.config["kiss"]["serial"].get("baudrate", 9600),
loop=self.loop, strip_df_start=True,
) )
elif "tcp" in self.config["kiss"] and self.config["kiss"]["tcp"].get( elif "tcp" in self.config["kiss"] and self.config["kiss"]["tcp"].get(
"enabled", "enabled",
False, False,
): ):
LOG.debug( LOG.debug(
"Setting up KISSTCP Connection to {}:{}".format( "KISS({}) TCP Connection to {}:{}".format(
kiss.__version__,
self.config["kiss"]["tcp"]["host"], self.config["kiss"]["tcp"]["host"],
self.config["kiss"]["tcp"]["port"], self.config["kiss"]["tcp"]["port"],
), ),
) )
self.kissdev = kiss.TCPKISSDevice( self.kiss = kiss.TCPKISS(
self.config["kiss"]["tcp"]["host"], host=self.config["kiss"]["tcp"]["host"],
self.config["kiss"]["tcp"]["port"], port=int(self.config["kiss"]["tcp"]["port"]),
loop=self.loop, strip_df_start=True,
log=LOG,
) )
self.kissdev.open() LOG.debug("Starting KISS interface connection")
self.kissport0 = self.kissdev[0] self.kiss.start()
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["kiss"]["callsign"],
log=LOG,
)
@trace.trace
def stop(self): def stop(self):
LOG.debug(self.kissdev) try:
self.kissdev._close() self.kiss.stop()
self.loop.stop() self.kiss.loop.call_soon_threadsafe(
self.kiss.protocol.transport.close,
)
except Exception as ex:
LOG.exception(ex)
def set_filter(self, filter): def set_filter(self, filter):
# This does nothing right now. # This does nothing right now.
pass pass
def consumer(self, callback, blocking=True, immortal=False, raw=False): def parse_frame(self, frame_bytes):
callsign = self.config["kiss"]["callsign"] frame = Frame.from_bytes(frame_bytes)
call = callsign.split("-") # Now parse it with aprslib
if len(call) > 1: packet = aprslib.parse(str(frame))
callsign = call[0] kwargs = {
ssid = int(call[1]) "frame": str(frame),
else: "packet": packet,
ssid = 0 }
self.aprsint.bind(callback=callback, callsign=callsign, ssid=ssid, regex=False) self._parse_callback(**kwargs)
self.loop.run_forever()
def consumer(self, callback, blocking=False, immortal=False, raw=False):
LOG.debug("Start blocking KISS consumer")
self._parse_callback = callback
self.kiss.read(callback=self.parse_frame, min_frames=None)
LOG.debug("END blocking KISS consumer")
def send(self, msg): def send(self, msg):
"""Send an APRS Message object.""" """Send an APRS Message object."""
payload = f"{msg._filter_for_send()}"
self.aprsint.send_message( # payload = (':%-9s:%s' % (
addressee=msg.tocall, # msg.tocall,
message=payload, # payload
# )).encode('US-ASCII'),
# payload = str(msg).encode('US-ASCII')
if isinstance(msg, messaging.AckMessage):
msg_payload = f"ack{msg.id}"
else:
msg_payload = f"{msg.message}{{{str(msg.id)}"
payload = (
":{:<9}:{}".format(
msg.tocall,
msg_payload,
)
).encode("US-ASCII")
LOG.debug(f"Send '{payload}' TO KISS")
frame = Frame.ui(
destination=msg.tocall,
source=msg.fromcall,
path=["WIDE1-1", "WIDE2-1"], path=["WIDE1-1", "WIDE2-1"],
oneshot=True, info=payload,
) )
self.kiss.write(frame)

View File

@ -8,8 +8,9 @@ import logging
import click import click
# local imports here # local imports here
from aprsd import cli_helper, client, messaging, packets, plugin, stats, trace from aprsd import cli_helper, client, messaging, packets, plugin, stats, utils
from aprsd.aprsd import cli from aprsd.aprsd import cli
from aprsd.utils import trace
LOG = logging.getLogger("APRSD") LOG = logging.getLogger("APRSD")
@ -69,6 +70,14 @@ def test_plugin(
"""Test an individual APRSD plugin given a python path.""" """Test an individual APRSD plugin given a python path."""
config = ctx.obj["config"] config = ctx.obj["config"]
flat_config = utils.flatten_dict(config)
LOG.info("Using CONFIG values:")
for x in flat_config:
if "password" in x or "aprsd.web.users.admin" in x:
LOG.info(f"{x} = XXXXXXXXXXXXXXXXXXX")
else:
LOG.info(f"{x} = {flat_config[x]}")
if not aprs_login: if not aprs_login:
if not config.exists("aprs.login"): if not config.exists("aprs.login"):
click.echo("Must set --aprs_login or APRS_LOGIN") click.echo("Must set --aprs_login or APRS_LOGIN")

View File

@ -14,10 +14,9 @@ from rich.console import Console
# local imports here # local imports here
import aprsd import aprsd
from aprsd import ( from aprsd import cli_helper, client, messaging, packets, stats, threads, utils
cli_helper, client, messaging, packets, stats, threads, trace, utils,
)
from aprsd.aprsd import cli from aprsd.aprsd import cli
from aprsd.utils import trace
# setup the global logger # setup the global logger
@ -140,19 +139,24 @@ def listen(
# Creates the client object # Creates the client object
LOG.info("Creating client connection") LOG.info("Creating client connection")
client.factory.create().client aprs_client = client.factory.create()
aprs_client = client.factory.create().client console.log(aprs_client)
LOG.debug(f"Filter by '{filter}'") LOG.debug(f"Filter by '{filter}'")
aprs_client.set_filter(filter) aprs_client.client.set_filter(filter)
packets.PacketList(config=config)
keepalive = threads.KeepAliveThread(config=config)
keepalive.start()
while True: while True:
try: try:
# This will register a packet consumer with aprslib # This will register a packet consumer with aprslib
# When new packets come in the consumer will process # When new packets come in the consumer will process
# the packet # the packet
with console.status("Listening for packets"): # with console.status("Listening for packets"):
aprs_client.consumer(rx_packet, raw=False) aprs_client.client.consumer(rx_packet, raw=False)
except aprslib.exceptions.ConnectionDrop: except aprslib.exceptions.ConnectionDrop:
LOG.error("Connection dropped, reconnecting") LOG.error("Connection dropped, reconnecting")
time.sleep(5) time.sleep(5)

View File

@ -7,10 +7,11 @@ import click
import aprsd import aprsd
from aprsd import ( from aprsd import (
cli_helper, client, flask, messaging, packets, plugin, stats, threads, cli_helper, client, flask, messaging, packets, plugin, stats, threads,
trace, utils, utils,
) )
from aprsd import aprsd as aprsd_main from aprsd import aprsd as aprsd_main
from aprsd.aprsd import cli from aprsd.aprsd import cli
from aprsd.threads import rx
LOG = logging.getLogger("APRSD") LOG = logging.getLogger("APRSD")
@ -58,8 +59,6 @@ def server(ctx, flush):
else: else:
LOG.info(f"{x} = {flat_config[x]}") LOG.info(f"{x} = {flat_config[x]}")
if config["aprsd"].get("trace", False):
trace.setup_tracing(["method", "api"])
stats.APRSDStats(config) stats.APRSDStats(config)
# Initialize the client factory and create # Initialize the client factory and create
@ -97,7 +96,7 @@ def server(ctx, flush):
plugin_manager = plugin.PluginManager(config) plugin_manager = plugin.PluginManager(config)
plugin_manager.setup_plugins() plugin_manager.setup_plugins()
rx_thread = threads.APRSDRXThread( rx_thread = rx.APRSDPluginRXThread(
msg_queues=threads.msg_queues, msg_queues=threads.msg_queues,
config=config, config=config,
) )

617
aprsd/cmds/webchat.py Normal file
View File

@ -0,0 +1,617 @@
import datetime
import json
import logging
from logging.handlers import RotatingFileHandler
import queue
import signal
import sys
import threading
import time
import aprslib
import click
import flask
from flask import request
from flask.logging import default_handler
import flask_classful
from flask_httpauth import HTTPBasicAuth
from flask_socketio import Namespace, SocketIO
from werkzeug.security import check_password_hash, generate_password_hash
import wrapt
import aprsd
from aprsd import cli_helper, client
from aprsd import config as aprsd_config
from aprsd import messaging, packets, stats, threads, utils
from aprsd.aprsd import cli
from aprsd.logging import rich as aprsd_logging
from aprsd.threads import aprsd as aprsd_thread
from aprsd.threads import rx
from aprsd.utils import objectstore, trace
LOG = logging.getLogger("APRSD")
auth = HTTPBasicAuth()
users = None
rx_msg_queue = queue.Queue(maxsize=20)
tx_msg_queue = queue.Queue(maxsize=20)
control_queue = queue.Queue(maxsize=20)
msg_queues = {
"rx": rx_msg_queue,
"control": control_queue,
"tx": tx_msg_queue,
}
def signal_handler(sig, frame):
click.echo("signal_handler: called")
LOG.info(
f"Ctrl+C, Sending all threads({len(threads.APRSDThreadList())}) exit! "
f"Can take up to 10 seconds {datetime.datetime.now()}",
)
threads.APRSDThreadList().stop_all()
if "subprocess" not in str(frame):
time.sleep(1.5)
# messaging.MsgTrack().save()
# packets.WatchList().save()
# packets.SeenList().save()
LOG.info(stats.APRSDStats())
LOG.info("Telling flask to bail.")
signal.signal(signal.SIGTERM, sys.exit(0))
sys.exit(0)
class SentMessages(objectstore.ObjectStoreMixin):
_instance = None
lock = threading.Lock()
data = {}
def __new__(cls, *args, **kwargs):
"""This magic turns this into a singleton."""
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance
@wrapt.synchronized(lock)
def add(self, msg):
self.data[msg.id] = self.create(msg.id)
self.data[msg.id]["from"] = msg.fromcall
self.data[msg.id]["to"] = msg.tocall
self.data[msg.id]["message"] = msg.message.rstrip("\n")
self.data[msg.id]["raw"] = str(msg).rstrip("\n")
def create(self, id):
return {
"id": id,
"ts": time.time(),
"ack": False,
"from": None,
"to": None,
"raw": None,
"message": None,
"status": None,
"last_update": None,
"reply": None,
}
@wrapt.synchronized(lock)
def __len__(self):
return len(self.data.keys())
@wrapt.synchronized(lock)
def get(self, id):
if id in self.data:
return self.data[id]
@wrapt.synchronized(lock)
def get_all(self):
return self.data
@wrapt.synchronized(lock)
def set_status(self, id, status):
if id in self.data:
self.data[id]["last_update"] = str(datetime.datetime.now())
self.data[id]["status"] = status
@wrapt.synchronized(lock)
def ack(self, id):
"""The message got an ack!"""
if id in self.data:
self.data[id]["last_update"] = str(datetime.datetime.now())
self.data[id]["ack"] = True
@wrapt.synchronized(lock)
def reply(self, id, packet):
"""We got a packet back from the sent message."""
if id in self.data:
self.data[id]["reply"] = packet
# HTTPBasicAuth doesn't work on a class method.
# This has to be out here. Rely on the APRSDFlask
# class to initialize the users from the config
@auth.verify_password
def verify_password(username, password):
global users
if username in users and check_password_hash(users.get(username), password):
return username
class WebChatRXThread(rx.APRSDRXThread):
"""Class that connects to aprsis/kiss and waits for messages."""
def connected(self, connected=True):
self.connected = connected
def stop(self):
self.thread_stop = True
client.factory.create().client.stop()
def loop(self):
# setup the consumer of messages and block until a messages
msg = None
try:
msg = self.msg_queues["tx"].get_nowait()
except queue.Empty:
pass
try:
if msg:
LOG.debug("GOT msg from TX queue!!")
msg.send()
except (
aprslib.exceptions.ConnectionDrop,
aprslib.exceptions.ConnectionError,
):
LOG.error("Connection dropped, reconnecting")
# Put it back on the queue to send.
self.msg_queues["tx"].put(msg)
# Force the deletion of the client object connected to aprs
# This will cause a reconnect, next time client.get_client()
# is called
self._client.reset()
time.sleep(2)
try:
# When new packets come in the consumer will process
# the packet
# This call blocks until thread stop() is called.
self._client.client.consumer(
self.process_packet, raw=False, blocking=False,
)
except (
aprslib.exceptions.ConnectionDrop,
aprslib.exceptions.ConnectionError,
):
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
self._client.reset()
return True
return True
def process_packet(self, *args, **kwargs):
# packet = self._client.decode_packet(*args, **kwargs)
if "packet" in kwargs:
packet = kwargs["packet"]
else:
packet = self._client.decode_packet(*args, **kwargs)
LOG.debug(f"GOT Packet {packet}")
self.msg_queues["rx"].put(packet)
class WebChatTXThread(aprsd_thread.APRSDThread):
"""Class that """
def __init__(self, msg_queues, config, socketio):
super().__init__("_TXThread_")
self.msg_queues = msg_queues
self.config = config
self.socketio = socketio
self.connected = False
def loop(self):
try:
msg = self.msg_queues["control"].get_nowait()
self.connected = msg["connected"]
except queue.Empty:
pass
try:
packet = self.msg_queues["rx"].get_nowait()
if packet:
# we got a packet and we need to send it to the
# web socket
self.process_packet(packet)
except queue.Empty:
pass
except Exception as ex:
LOG.exception(ex)
time.sleep(1)
return True
def process_ack_packet(self, packet):
ack_num = packet.get("msgNo")
LOG.info(f"We got ack for our sent message {ack_num}")
messaging.log_packet(packet)
SentMessages().ack(int(ack_num))
self.socketio.emit(
"ack", SentMessages().get(int(ack_num)),
namespace="/sendmsg",
)
stats.APRSDStats().ack_rx_inc()
self.got_ack = True
def process_packet(self, packet):
LOG.info(f"process PACKET {packet}")
tocall = packet.get("addresse", None)
fromcall = packet["from"]
msg = packet.get("message_text", None)
msg_id = packet.get("msgNo", "0")
msg_response = packet.get("response", None)
if tocall == self.config["aprsd"]["callsign"] and msg_response == "ack":
self.process_ack_packet(packet)
elif tocall == self.config["aprsd"]["callsign"]:
messaging.log_message(
"Received Message",
packet["raw"],
msg,
fromcall=fromcall,
msg_num=msg_id,
)
# let any threads do their thing, then ack
# send an ack last
ack = messaging.AckMessage(
self.config["aprsd"]["callsign"],
fromcall,
msg_id=msg_id,
)
ack.send()
packets.PacketList().add(packet)
stats.APRSDStats().msgs_rx_inc()
message = packet.get("message_text", None)
msg = {
"id": 0,
"ts": time.time(),
"ack": False,
"from": fromcall,
"to": packet["to"],
"raw": packet["raw"],
"message": message,
"status": None,
"last_update": None,
"reply": None,
}
self.socketio.emit(
"new", msg,
namespace="/sendmsg",
)
class WebChatFlask(flask_classful.FlaskView):
config = None
def set_config(self, config):
global users
self.config = config
self.users = {}
for user in self.config["aprsd"]["web"]["users"]:
self.users[user] = generate_password_hash(
self.config["aprsd"]["web"]["users"][user],
)
users = self.users
@auth.login_required
def index(self):
stats = self._stats()
if self.config["aprs"].get("enabled", True):
transport = "aprs-is"
aprs_connection = (
"APRS-IS Server: <a href='http://status.aprs2.net' >"
"{}</a>".format(stats["stats"]["aprs-is"]["server"])
)
else:
# We might be connected to a KISS socket?
if client.KISSClient.is_enabled(self.config):
transport = client.KISSClient.transport(self.config)
if transport == client.TRANSPORT_TCPKISS:
aprs_connection = (
"TCPKISS://{}:{}".format(
self.config["kiss"]["tcp"]["host"],
self.config["kiss"]["tcp"]["port"],
)
)
elif transport == client.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
LOG.debug(f"initial stats = {stats}")
return flask.render_template(
"index.html",
initial_stats=stats,
aprs_connection=aprs_connection,
callsign=self.config["aprsd"]["callsign"],
version=aprsd.__version__,
)
@auth.login_required
def send_message_status(self):
LOG.debug(request)
msgs = SentMessages()
info = msgs.get_all()
return json.dumps(info)
def _stats(self):
stats_obj = stats.APRSDStats()
now = datetime.datetime.now()
time_format = "%m-%d-%Y %H:%M:%S"
stats_dict = stats_obj.stats()
# Webchat doesnt need these
del stats_dict["aprsd"]["watch_list"]
del stats_dict["aprsd"]["seen_list"]
# del stats_dict["email"]
# del stats_dict["plugins"]
# del stats_dict["messages"]
result = {
"time": now.strftime(time_format),
"stats": stats_dict,
}
return result
def stats(self):
return json.dumps(self._stats())
class SendMessageNamespace(Namespace):
"""Class to handle the socketio interactions."""
_config = None
got_ack = False
reply_sent = False
msg = None
request = None
def __init__(self, namespace=None, config=None, msg_queues=None):
self._config = config
self._msg_queues = msg_queues
super().__init__(namespace)
def on_connect(self):
global socketio
LOG.debug("Web socket connected")
socketio.emit(
"connected", {"data": "/sendmsg Connected"},
namespace="/sendmsg",
)
msg = {"connected": True}
self._msg_queues["control"].put(msg)
def on_disconnect(self):
LOG.debug("WS Disconnected")
msg = {"connected": False}
self._msg_queues["control"].put(msg)
def on_send(self, data):
global socketio
LOG.debug(f"WS: on_send {data}")
self.request = data
data["from"] = self._config["aprs"]["login"]
msg = messaging.TextMessage(
data["from"],
data["to"],
data["message"],
)
self.msg = msg
msgs = SentMessages()
msgs.add(msg)
msgs.set_status(msg.id, "Sending")
obj = msgs.get(self.msg.id)
socketio.emit(
"sent", obj,
namespace="/sendmsg",
)
msg.send()
# self._msg_queues["tx"].put(msg)
def handle_message(self, data):
LOG.debug(f"WS Data {data}")
def handle_json(self, data):
LOG.debug(f"WS json {data}")
def setup_logging(config, flask_app, loglevel, quiet):
flask_log = logging.getLogger("werkzeug")
flask_app.logger.removeHandler(default_handler)
flask_log.removeHandler(default_handler)
log_level = aprsd_config.LOG_LEVELS[loglevel]
flask_log.setLevel(log_level)
date_format = config["aprsd"].get(
"dateformat",
aprsd_config.DEFAULT_DATE_FORMAT,
)
if not config["aprsd"]["web"].get("logging_enabled", False):
# disable web logging
flask_log.disabled = True
flask_app.logger.disabled = True
# return
if config["aprsd"].get("rich_logging", False) and not quiet:
log_format = "%(message)s"
log_formatter = logging.Formatter(fmt=log_format, datefmt=date_format)
rh = aprsd_logging.APRSDRichHandler(
show_thread=True, thread_width=15,
rich_tracebacks=True, omit_repeated_times=False,
)
rh.setFormatter(log_formatter)
flask_log.addHandler(rh)
log_file = config["aprsd"].get("logfile", None)
if log_file:
log_format = config["aprsd"].get(
"logformat",
aprsd_config.DEFAULT_LOG_FORMAT,
)
log_formatter = logging.Formatter(fmt=log_format, datefmt=date_format)
fh = RotatingFileHandler(
log_file, maxBytes=(10248576 * 5),
backupCount=4,
)
fh.setFormatter(log_formatter)
flask_log.addHandler(fh)
@trace.trace
def init_flask(config, loglevel, quiet):
global socketio
flask_app = flask.Flask(
"aprsd",
static_url_path="/static",
static_folder="web/chat/static",
template_folder="web/chat/templates",
)
setup_logging(config, flask_app, loglevel, quiet)
server = WebChatFlask()
server.set_config(config)
flask_app.route("/", methods=["GET"])(server.index)
flask_app.route("/stats", methods=["GET"])(server.stats)
# flask_app.route("/send-message", methods=["GET"])(server.send_message)
flask_app.route("/send-message-status", methods=["GET"])(server.send_message_status)
socketio = SocketIO(
flask_app, logger=False, engineio_logger=False,
async_mode="threading",
)
# async_mode="gevent",
# async_mode="eventlet",
# import eventlet
# eventlet.monkey_patch()
socketio.on_namespace(
SendMessageNamespace(
"/sendmsg", config=config,
msg_queues=msg_queues,
),
)
return socketio, flask_app
# main() ###
@cli.command()
@cli_helper.add_options(cli_helper.common_options)
@click.option(
"-f",
"--flush",
"flush",
is_flag=True,
show_default=True,
default=False,
help="Flush out all old aged messages on disk.",
)
@click.option(
"-p",
"--port",
"port",
show_default=True,
default=80,
help="Port to listen to web requests",
)
@click.pass_context
@cli_helper.process_standard_options
def webchat(ctx, flush, port):
"""Web based HAM Radio chat program!"""
ctx.obj["config_file"]
loglevel = ctx.obj["loglevel"]
quiet = ctx.obj["quiet"]
config = ctx.obj["config"]
signal.signal(signal.SIGINT, signal_handler)
signal.signal(signal.SIGTERM, signal_handler)
if not quiet:
click.echo("Load config")
level, msg = utils._check_version()
if level:
LOG.warning(msg)
else:
LOG.info(msg)
LOG.info(f"APRSD Started version: {aprsd.__version__}")
flat_config = utils.flatten_dict(config)
LOG.info("Using CONFIG values:")
for x in flat_config:
if "password" in x or "aprsd.web.users.admin" in x:
LOG.info(f"{x} = XXXXXXXXXXXXXXXXXXX")
else:
LOG.info(f"{x} = {flat_config[x]}")
stats.APRSDStats(config)
# Initialize the client factory and create
# The correct client object ready for use
client.ClientFactory.setup(config)
# Make sure we have 1 client transport enabled
if not client.factory.is_client_enabled():
LOG.error("No Clients are enabled in config.")
sys.exit(-1)
if not client.factory.is_client_configured():
LOG.error("APRS client is not properly configured in config file.")
sys.exit(-1)
packets.PacketList(config=config)
messaging.MsgTrack(config=config)
packets.WatchList(config=config)
packets.SeenList(config=config)
(socketio, app) = init_flask(config, loglevel, quiet)
rx_thread = WebChatRXThread(
msg_queues=msg_queues,
config=config,
)
LOG.info("Start RX Thread")
rx_thread.start()
tx_thread = WebChatTXThread(
msg_queues=msg_queues,
config=config,
socketio=socketio,
)
LOG.info("Start TX Thread")
tx_thread.start()
keepalive = threads.KeepAliveThread(config=config)
LOG.info("Start KeepAliveThread")
keepalive.start()
LOG.info("Start socketio.run()")
socketio.run(
app,
host=config["aprsd"]["web"]["host"],
port=port,
)
LOG.info("WebChat exiting!!!! Bye.")

View File

@ -57,13 +57,13 @@ DEFAULT_CONFIG_DICT = {
"ham": {"callsign": "NOCALL"}, "ham": {"callsign": "NOCALL"},
"aprs": { "aprs": {
"enabled": True, "enabled": True,
# Only used as the login for aprsis.
"login": "CALLSIGN", "login": "CALLSIGN",
"password": "00000", "password": "00000",
"host": "rotate.aprs2.net", "host": "rotate.aprs2.net",
"port": 14580, "port": 14580,
}, },
"kiss": { "kiss": {
"callsign": "NOCALL",
"tcp": { "tcp": {
"enabled": False, "enabled": False,
"host": "direwolf.ip.address", "host": "direwolf.ip.address",
@ -76,11 +76,14 @@ DEFAULT_CONFIG_DICT = {
}, },
}, },
"aprsd": { "aprsd": {
# Callsign to use for all packets to/from aprsd instance
# regardless of the client (aprsis vs kiss)
"callsign": "NOCALL",
"logfile": "/tmp/aprsd.log", "logfile": "/tmp/aprsd.log",
"logformat": DEFAULT_LOG_FORMAT, "logformat": DEFAULT_LOG_FORMAT,
"dateformat": DEFAULT_DATE_FORMAT, "dateformat": DEFAULT_DATE_FORMAT,
"save_location": DEFAULT_CONFIG_DIR, "save_location": DEFAULT_CONFIG_DIR,
"rich_logging": False, "rich_logging": True,
"trace": False, "trace": False,
"enabled_plugins": CORE_MESSAGE_PLUGINS, "enabled_plugins": CORE_MESSAGE_PLUGINS,
"units": "imperial", "units": "imperial",
@ -177,16 +180,35 @@ class Config(collections.UserDict):
if not self.exists(path): if not self.exists(path):
if type(path) is list: if type(path) is list:
path = ".".join(path) path = ".".join(path)
raise exception.MissingConfigOption(path) raise exception.MissingConfigOptionException(path)
val = self.get(path) val = self.get(path)
if val == default_fail: if val == default_fail:
# We have to fail and bail if the user hasn't edited # We have to fail and bail if the user hasn't edited
# this config option. # this config option.
raise exception.ConfigOptionBogusDefaultException(path, default_fail) raise exception.ConfigOptionBogusDefaultException(
path, default_fail,
)
def add_config_comments(raw_yaml): def add_config_comments(raw_yaml):
end_idx = utils.end_substr(raw_yaml, "ham:")
if end_idx != -1:
# lets insert a comment
raw_yaml = utils.insert_str(
raw_yaml,
"\n # Callsign that owns this instance of APRSD.",
end_idx,
)
end_idx = utils.end_substr(raw_yaml, "aprsd:")
if end_idx != -1:
# lets insert a comment
raw_yaml = utils.insert_str(
raw_yaml,
"\n # Callsign to use for all APRSD Packets as the to/from."
"\n # regardless of client type (aprsis vs tcpkiss vs serial)",
end_idx,
)
end_idx = utils.end_substr(raw_yaml, "aprs:") end_idx = utils.end_substr(raw_yaml, "aprs:")
if end_idx != -1: if end_idx != -1:
# lets insert a comment # lets insert a comment
@ -326,6 +348,11 @@ def parse_config(config_file):
config, config,
["aprsd"], ["aprsd"],
) )
check_option(
config,
"aprsd.callsign",
default_fail=DEFAULT_CONFIG_DICT["aprsd"]["callsign"],
)
# Ensure they change the admin password # Ensure they change the admin password
if config.get("aprsd.web.enabled") is True: if config.get("aprsd.web.enabled") is True:

View File

@ -18,9 +18,10 @@ from werkzeug.security import check_password_hash, generate_password_hash
import aprsd import aprsd
from aprsd import client from aprsd import client
from aprsd import config as aprsd_config from aprsd import config as aprsd_config
from aprsd import log, messaging, packets, plugin, stats, threads, utils from aprsd import messaging, packets, plugin, stats, threads, utils
from aprsd.clients import aprsis from aprsd.clients import aprsis
from aprsd.logging import logging as aprsd_logging from aprsd.logging import log
from aprsd.logging import rich as aprsd_logging
LOG = logging.getLogger("APRSD") LOG = logging.getLogger("APRSD")
@ -600,8 +601,8 @@ def init_flask(config, loglevel, quiet):
flask_app = flask.Flask( flask_app = flask.Flask(
"aprsd", "aprsd",
static_url_path="/static", static_url_path="/static",
static_folder="web/static", static_folder="web/admin/static",
template_folder="web/templates", template_folder="web/admin/templates",
) )
setup_logging(config, flask_app, loglevel, quiet) setup_logging(config, flask_app, loglevel, quiet)
server = APRSDFlask() server = APRSDFlask()

View File

@ -5,7 +5,7 @@ import queue
import sys import sys
from aprsd import config as aprsd_config from aprsd import config as aprsd_config
from aprsd.logging import logging as aprsd_logging from aprsd.logging import rich as aprsd_logging
LOG = logging.getLogger("APRSD") LOG = logging.getLogger("APRSD")

View File

@ -6,7 +6,8 @@ import re
import threading import threading
import time import time
from aprsd import client, objectstore, packets, stats, threads from aprsd import client, packets, stats, threads
from aprsd.utils import objectstore
LOG = logging.getLogger("APRSD") LOG = logging.getLogger("APRSD")
@ -238,7 +239,10 @@ class RawMessage(Message):
last_send_age = last_send_time = None last_send_age = last_send_time = None
def __init__(self, message, allow_delay=True): def __init__(self, message, allow_delay=True):
super().__init__(fromcall=None, tocall=None, msg_id=None, allow_delay=allow_delay) super().__init__(
fromcall=None, tocall=None, msg_id=None,
allow_delay=allow_delay,
)
self._raw_message = message self._raw_message = message
def dict(self): def dict(self):
@ -282,12 +286,8 @@ class TextMessage(Message):
last_send_time = last_send_age = None last_send_time = last_send_age = None
def __init__( def __init__(
self, self, fromcall, tocall, message,
fromcall, msg_id=None, allow_delay=True,
tocall,
message,
msg_id=None,
allow_delay=True,
): ):
super().__init__( super().__init__(
fromcall=fromcall, tocall=tocall, fromcall=fromcall, tocall=tocall,

View File

@ -3,7 +3,10 @@ import logging
import threading import threading
import time import time
from aprsd import objectstore, utils import wrapt
from aprsd import utils
from aprsd.utils import objectstore
LOG = logging.getLogger("APRSD") LOG = logging.getLogger("APRSD")
@ -17,6 +20,7 @@ class PacketList:
"""Class to track all of the packets rx'd and tx'd by aprsd.""" """Class to track all of the packets rx'd and tx'd by aprsd."""
_instance = None _instance = None
lock = threading.Lock()
config = None config = None
packet_list = {} packet_list = {}
@ -28,7 +32,6 @@ class PacketList:
if cls._instance is None: if cls._instance is None:
cls._instance = super().__new__(cls) cls._instance = super().__new__(cls)
cls._instance.packet_list = utils.RingBuffer(1000) cls._instance.packet_list = utils.RingBuffer(1000)
cls._instance.lock = threading.Lock()
cls._instance.config = kwargs["config"] cls._instance.config = kwargs["config"]
return cls._instance return cls._instance
@ -36,50 +39,51 @@ class PacketList:
if config: if config:
self.config = config self.config = config
@wrapt.synchronized(lock)
def __iter__(self): def __iter__(self):
with self.lock: return iter(self.packet_list)
return iter(self.packet_list)
@wrapt.synchronized(lock)
def add(self, packet): def add(self, packet):
with self.lock: packet["ts"] = time.time()
packet["ts"] = time.time() if (
if ( "fromcall" in packet
"fromcall" in packet and packet["fromcall"] == self.config["aprs"]["login"]
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 self.packet_list.append(packet)
self.packet_list.append(packet) SeenList().update_seen(packet)
SeenList().update_seen(packet)
@wrapt.synchronized(lock)
def get(self): def get(self):
with self.lock: return self.packet_list.get()
return self.packet_list.get()
@wrapt.synchronized(lock)
def total_received(self): def total_received(self):
with self.lock: return self.total_recv
return self.total_recv
@wrapt.synchronized(lock)
def total_sent(self): def total_sent(self):
with self.lock: return self.total_tx
return self.total_tx
class WatchList(objectstore.ObjectStoreMixin): class WatchList(objectstore.ObjectStoreMixin):
"""Global watch list and info for callsigns.""" """Global watch list and info for callsigns."""
_instance = None _instance = None
lock = threading.Lock()
data = {} data = {}
config = None config = None
def __new__(cls, *args, **kwargs): def __new__(cls, *args, **kwargs):
if cls._instance is None: if cls._instance is None:
cls._instance = super().__new__(cls) cls._instance = super().__new__(cls)
cls._instance.lock = threading.Lock() if "config" in kwargs:
cls._instance.config = kwargs["config"] cls._instance.config = kwargs["config"]
cls._instance._init_store()
cls._instance.data = {} cls._instance.data = {}
cls._instance._init_store()
return cls._instance return cls._instance
def __init__(self, config=None): def __init__(self, config=None):
@ -110,12 +114,12 @@ class WatchList(objectstore.ObjectStoreMixin):
def callsign_in_watchlist(self, callsign): def callsign_in_watchlist(self, callsign):
return callsign in self.data return callsign in self.data
@wrapt.synchronized(lock)
def update_seen(self, packet): def update_seen(self, packet):
with self.lock: callsign = packet["from"]
callsign = packet["from"] 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)
def last_seen(self, callsign): def last_seen(self, callsign):
if self.callsign_in_watchlist(callsign): if self.callsign_in_watchlist(callsign):
@ -158,18 +162,20 @@ class SeenList(objectstore.ObjectStoreMixin):
"""Global callsign seen list.""" """Global callsign seen list."""
_instance = None _instance = None
lock = threading.Lock()
data = {} data = {}
config = None config = None
def __new__(cls, *args, **kwargs): def __new__(cls, *args, **kwargs):
if cls._instance is None: if cls._instance is None:
cls._instance = super().__new__(cls) cls._instance = super().__new__(cls)
cls._instance.lock = threading.Lock() if "config" in kwargs:
cls._instance.config = kwargs["config"] cls._instance.config = kwargs["config"]
cls._instance._init_store()
cls._instance.data = {} cls._instance.data = {}
cls._instance._init_store()
return cls._instance return cls._instance
@wrapt.synchronized(lock)
def update_seen(self, packet): def update_seen(self, packet):
callsign = None callsign = None
if "fromcall" in packet: if "fromcall" in packet:

View File

@ -492,4 +492,5 @@ class PluginManager:
self._pluggy_pm.register(obj) self._pluggy_pm.register(obj)
def get_plugins(self): def get_plugins(self):
return self._pluggy_pm.get_plugins() if self._pluggy_pm:
return self._pluggy_pm.get_plugins()

View File

@ -11,7 +11,8 @@ import time
import imapclient import imapclient
from validate_email import validate_email from validate_email import validate_email
from aprsd import messaging, plugin, stats, threads, trace from aprsd import messaging, plugin, stats, threads
from aprsd.utils import trace
LOG = logging.getLogger("APRSD") LOG = logging.getLogger("APRSD")

View File

@ -2,7 +2,8 @@ import logging
import shutil import shutil
import subprocess import subprocess
from aprsd import plugin, trace from aprsd import plugin
from aprsd.utils import trace
LOG = logging.getLogger("APRSD") LOG = logging.getLogger("APRSD")

View File

@ -2,7 +2,8 @@ import logging
import re import re
import time import time
from aprsd import plugin, plugin_utils, trace from aprsd import plugin, plugin_utils
from aprsd.utils import trace
LOG = logging.getLogger("APRSD") LOG = logging.getLogger("APRSD")

View File

@ -1,6 +1,7 @@
import logging import logging
from aprsd import messaging, packets, plugin, trace from aprsd import messaging, packets, plugin
from aprsd.utils import trace
LOG = logging.getLogger("APRSD") LOG = logging.getLogger("APRSD")

View File

@ -1,7 +1,8 @@
import logging import logging
import time import time
from aprsd import plugin, trace from aprsd import plugin
from aprsd.utils import trace
LOG = logging.getLogger("APRSD") LOG = logging.getLogger("APRSD")

View File

@ -2,7 +2,8 @@ import datetime
import logging import logging
import re import re
from aprsd import messaging, plugin, trace from aprsd import messaging, plugin
from aprsd.utils import trace
LOG = logging.getLogger("APRSD") LOG = logging.getLogger("APRSD")

View File

@ -2,10 +2,10 @@ import logging
import re import re
import time import time
from opencage.geocoder import OpenCageGeocode
import pytz import pytz
from aprsd import fuzzyclock, plugin, plugin_utils, trace from aprsd import plugin, plugin_utils
from aprsd.utils import fuzzy, trace
LOG = logging.getLogger("APRSD") LOG = logging.getLogger("APRSD")
@ -32,7 +32,7 @@ class TimePlugin(plugin.APRSDRegexCommandPluginBase):
local_short_str = local_t.strftime("%H:%M %Z") local_short_str = local_t.strftime("%H:%M %Z")
local_hour = local_t.strftime("%H") local_hour = local_t.strftime("%H")
local_min = local_t.strftime("%M") local_min = local_t.strftime("%M")
cur_time = fuzzyclock.fuzzy(int(local_hour), int(local_min), 1) cur_time = fuzzy(int(local_hour), int(local_min), 1)
reply = "{} ({})".format( reply = "{} ({})".format(
cur_time, cur_time,
@ -49,68 +49,6 @@ class TimePlugin(plugin.APRSDRegexCommandPluginBase):
return self.build_date_str(localzone) return self.build_date_str(localzone)
class TimeOpenCageDataPlugin(TimePlugin, plugin.APRSFIKEYMixin):
"""geocage based timezone fetching."""
command_regex = "^[tT]"
command_name = "time"
short_description = "Current time of GPS beacon timezone. Uses OpenCage"
def setup(self):
self.ensure_aprs_fi_key()
@trace.trace
def process(self, packet):
fromcall = packet.get("from")
message = packet.get("message_text", None)
# ack = packet.get("msgNo", "0")
api_key = self.config["services"]["aprs.fi"]["apiKey"]
# optional second argument is a callsign to search
a = re.search(r"^.*\s+(.*)", message)
if a is not None:
searchcall = a.group(1)
searchcall = searchcall.upper()
else:
# if no second argument, search for calling station
searchcall = fromcall
try:
aprs_data = plugin_utils.get_aprs_fi(api_key, searchcall)
except Exception as ex:
LOG.error(f"Failed to fetch aprs.fi data {ex}")
return "Failed to fetch location"
# LOG.debug("LocationPlugin: aprs_data = {}".format(aprs_data))
if not len(aprs_data["entries"]):
LOG.error("Didn't get any entries from aprs.fi")
return "Failed to fetch aprs.fi location"
lat = aprs_data["entries"][0]["lat"]
lon = aprs_data["entries"][0]["lng"]
try:
self.config.exists("opencagedata.apiKey")
except Exception as ex:
LOG.error(f"Failed to find config opencage:apiKey {ex}")
return "No opencage apiKey found"
try:
opencage_key = self.config["opencagedata"]["apiKey"]
geocoder = OpenCageGeocode(opencage_key)
results = geocoder.reverse_geocode(lat, lon)
except Exception as ex:
LOG.error(f"Couldn't fetch opencagedata api '{ex}'")
# Default to UTC instead
localzone = pytz.timezone("UTC")
else:
tzone = results[0]["annotations"]["timezone"]["name"]
localzone = pytz.timezone(tzone)
return self.build_date_str(localzone)
class TimeOWMPlugin(TimePlugin, plugin.APRSFIKEYMixin): class TimeOWMPlugin(TimePlugin, plugin.APRSFIKEYMixin):
"""OpenWeatherMap based timezone fetching.""" """OpenWeatherMap based timezone fetching."""

View File

@ -1,7 +1,8 @@
import logging import logging
import aprsd import aprsd
from aprsd import plugin, stats, trace from aprsd import plugin, stats
from aprsd.utils import trace
LOG = logging.getLogger("APRSD") LOG = logging.getLogger("APRSD")

View File

@ -4,7 +4,8 @@ import re
import requests import requests
from aprsd import plugin, plugin_utils, trace from aprsd import plugin, plugin_utils
from aprsd.utils import trace
LOG = logging.getLogger("APRSD") LOG = logging.getLogger("APRSD")

View File

@ -2,6 +2,8 @@ import datetime
import logging import logging
import threading import threading
import wrapt
import aprsd import aprsd
from aprsd import packets, plugin, utils from aprsd import packets, plugin, utils
@ -12,7 +14,7 @@ LOG = logging.getLogger("APRSD")
class APRSDStats: class APRSDStats:
_instance = None _instance = None
lock = None lock = threading.Lock()
config = None config = None
start_time = None start_time = None
@ -39,7 +41,6 @@ class APRSDStats:
if cls._instance is None: if cls._instance is None:
cls._instance = super().__new__(cls) cls._instance = super().__new__(cls)
# any initializetion here # any initializetion here
cls._instance.lock = threading.Lock()
cls._instance.start_time = datetime.datetime.now() cls._instance.start_time = datetime.datetime.now()
cls._instance._aprsis_keepalive = datetime.datetime.now() cls._instance._aprsis_keepalive = datetime.datetime.now()
return cls._instance return cls._instance
@ -48,128 +49,129 @@ class APRSDStats:
if config: if config:
self.config = config self.config = config
@wrapt.synchronized(lock)
@property @property
def uptime(self): def uptime(self):
with self.lock: return datetime.datetime.now() - self.start_time
return datetime.datetime.now() - self.start_time
@wrapt.synchronized(lock)
@property @property
def memory(self): def memory(self):
with self.lock: return self._mem_current
return self._mem_current
@wrapt.synchronized(lock)
def set_memory(self, memory): def set_memory(self, memory):
with self.lock: self._mem_current = memory
self._mem_current = memory
@wrapt.synchronized(lock)
@property @property
def memory_peak(self): def memory_peak(self):
with self.lock: return self._mem_peak
return self._mem_peak
@wrapt.synchronized(lock)
def set_memory_peak(self, memory): def set_memory_peak(self, memory):
with self.lock: self._mem_peak = memory
self._mem_peak = memory
@wrapt.synchronized(lock)
@property @property
def aprsis_server(self): def aprsis_server(self):
with self.lock: return self._aprsis_server
return self._aprsis_server
@wrapt.synchronized(lock)
def set_aprsis_server(self, server): def set_aprsis_server(self, server):
with self.lock: self._aprsis_server = server
self._aprsis_server = server
@wrapt.synchronized(lock)
@property @property
def aprsis_keepalive(self): def aprsis_keepalive(self):
with self.lock: return self._aprsis_keepalive
return self._aprsis_keepalive
@wrapt.synchronized(lock)
def set_aprsis_keepalive(self): def set_aprsis_keepalive(self):
with self.lock: self._aprsis_keepalive = datetime.datetime.now()
self._aprsis_keepalive = datetime.datetime.now()
@wrapt.synchronized(lock)
@property @property
def msgs_tx(self): def msgs_tx(self):
with self.lock: return self._msgs_tx
return self._msgs_tx
@wrapt.synchronized(lock)
def msgs_tx_inc(self): def msgs_tx_inc(self):
with self.lock: self._msgs_tx += 1
self._msgs_tx += 1
@wrapt.synchronized(lock)
@property @property
def msgs_rx(self): def msgs_rx(self):
with self.lock: return self._msgs_rx
return self._msgs_rx
@wrapt.synchronized(lock)
def msgs_rx_inc(self): def msgs_rx_inc(self):
with self.lock: self._msgs_rx += 1
self._msgs_rx += 1
@wrapt.synchronized(lock)
@property @property
def msgs_mice_rx(self): def msgs_mice_rx(self):
with self.lock: return self._msgs_mice_rx
return self._msgs_mice_rx
@wrapt.synchronized(lock)
def msgs_mice_inc(self): def msgs_mice_inc(self):
with self.lock: self._msgs_mice_rx += 1
self._msgs_mice_rx += 1
@wrapt.synchronized(lock)
@property @property
def ack_tx(self): def ack_tx(self):
with self.lock: return self._ack_tx
return self._ack_tx
@wrapt.synchronized(lock)
def ack_tx_inc(self): def ack_tx_inc(self):
with self.lock: self._ack_tx += 1
self._ack_tx += 1
@wrapt.synchronized(lock)
@property @property
def ack_rx(self): def ack_rx(self):
with self.lock: return self._ack_rx
return self._ack_rx
@wrapt.synchronized(lock)
def ack_rx_inc(self): def ack_rx_inc(self):
with self.lock: self._ack_rx += 1
self._ack_rx += 1
@wrapt.synchronized(lock)
@property @property
def msgs_tracked(self): def msgs_tracked(self):
with self.lock: return self._msgs_tracked
return self._msgs_tracked
@wrapt.synchronized(lock)
def msgs_tracked_inc(self): def msgs_tracked_inc(self):
with self.lock: self._msgs_tracked += 1
self._msgs_tracked += 1
@wrapt.synchronized(lock)
@property @property
def email_tx(self): def email_tx(self):
with self.lock: return self._email_tx
return self._email_tx
@wrapt.synchronized(lock)
def email_tx_inc(self): def email_tx_inc(self):
with self.lock: self._email_tx += 1
self._email_tx += 1
@wrapt.synchronized(lock)
@property @property
def email_rx(self): def email_rx(self):
with self.lock: return self._email_rx
return self._email_rx
@wrapt.synchronized(lock)
def email_rx_inc(self): def email_rx_inc(self):
with self.lock: self._email_rx += 1
self._email_rx += 1
@wrapt.synchronized(lock)
@property @property
def email_thread_time(self): def email_thread_time(self):
with self.lock: return self._email_thread_last_time
return self._email_thread_last_time
@wrapt.synchronized(lock)
def email_thread_update(self): def email_thread_update(self):
with self.lock: self._email_thread_last_time = datetime.datetime.now()
self._email_thread_last_time = datetime.datetime.now()
@wrapt.synchronized(lock)
def stats(self): def stats(self):
now = datetime.datetime.now() now = datetime.datetime.now()
if self._email_thread_last_time: if self._email_thread_last_time:
@ -185,20 +187,20 @@ class APRSDStats:
pm = plugin.PluginManager() pm = plugin.PluginManager()
plugins = pm.get_plugins() plugins = pm.get_plugins()
plugin_stats = {} plugin_stats = {}
if plugins:
def full_name_with_qualname(obj):
return "{}.{}".format(
obj.__class__.__module__,
obj.__class__.__qualname__,
)
def full_name_with_qualname(obj): for p in plugins:
return "{}.{}".format( plugin_stats[full_name_with_qualname(p)] = {
obj.__class__.__module__, "enabled": p.enabled,
obj.__class__.__qualname__, "rx": p.rx_count,
) "tx": p.tx_count,
"version": p.version,
for p in plugins: }
plugin_stats[full_name_with_qualname(p)] = {
"enabled": p.enabled,
"rx": p.rx_count,
"tx": p.tx_count,
"version": p.version,
}
wl = packets.WatchList() wl = packets.WatchList()
sl = packets.SeenList() sl = packets.SeenList()
@ -207,30 +209,30 @@ class APRSDStats:
"aprsd": { "aprsd": {
"version": aprsd.__version__, "version": aprsd.__version__,
"uptime": utils.strfdelta(self.uptime), "uptime": utils.strfdelta(self.uptime),
"memory_current": self.memory, "memory_current": int(self.memory),
"memory_current_str": utils.human_size(self.memory), "memory_current_str": utils.human_size(self.memory),
"memory_peak": self.memory_peak, "memory_peak": int(self.memory_peak),
"memory_peak_str": utils.human_size(self.memory_peak), "memory_peak_str": utils.human_size(self.memory_peak),
"watch_list": wl.get_all(), "watch_list": wl.get_all(),
"seen_list": sl.get_all(), "seen_list": sl.get_all(),
}, },
"aprs-is": { "aprs-is": {
"server": self.aprsis_server, "server": str(self.aprsis_server),
"callsign": self.config["aprs"]["login"], "callsign": self.config["aprs"]["login"],
"last_update": last_aprsis_keepalive, "last_update": last_aprsis_keepalive,
}, },
"messages": { "messages": {
"tracked": self.msgs_tracked, "tracked": int(self.msgs_tracked),
"sent": self.msgs_tx, "sent": int(self.msgs_tx),
"recieved": self.msgs_rx, "recieved": int(self.msgs_rx),
"ack_sent": self.ack_tx, "ack_sent": int(self.ack_tx),
"ack_recieved": self.ack_rx, "ack_recieved": int(self.ack_rx),
"mic-e recieved": self.msgs_mice_rx, "mic-e recieved": int(self.msgs_mice_rx),
}, },
"email": { "email": {
"enabled": self.config["aprsd"]["email"]["enabled"], "enabled": self.config["aprsd"]["email"]["enabled"],
"sent": self._email_tx, "sent": int(self._email_tx),
"recieved": self._email_rx, "recieved": int(self._email_rx),
"thread_last_update": last_update, "thread_last_update": last_update,
}, },
"plugins": plugin_stats, "plugins": plugin_stats,

13
aprsd/threads/__init__.py Normal file
View File

@ -0,0 +1,13 @@
import queue
# Make these available to anyone importing
# aprsd.threads
from .aprsd import APRSDThread, APRSDThreadList # noqa: F401
from .keep_alive import KeepAliveThread # noqa: F401
from .rx import APRSDRXThread # noqa: F401
rx_msg_queue = queue.Queue(maxsize=20)
msg_queues = {
"rx": rx_msg_queue,
}

94
aprsd/threads/aprsd.py Normal file
View File

@ -0,0 +1,94 @@
import abc
import logging
from queue import Queue
import threading
import wrapt
LOG = logging.getLogger("APRSD")
class APRSDThreadList:
"""Singleton class that keeps track of application wide threads."""
_instance = None
threads_list = []
lock = threading.Lock()
global_queue = Queue()
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):
thread_obj.set_global_queue(self.global_queue)
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):
self.global_queue.put_nowait({"quit": True})
"""Iterate over all threads and call stop on them."""
for th in self.threads_list:
LOG.info(f"Stopping Thread {th.name}")
th.stop()
@wrapt.synchronized(lock)
def __len__(self):
return len(self.threads_list)
class APRSDThread(threading.Thread, metaclass=abc.ABCMeta):
global_queue = None
def __init__(self, name):
super().__init__(name=name)
self.thread_stop = False
APRSDThreadList().add(self)
def set_global_queue(self, global_queue):
self.global_queue = global_queue
def _should_quit(self):
""" see if we have a quit message from the global queue."""
if self.thread_stop:
return True
if self.global_queue.empty():
return False
msg = self.global_queue.get(timeout=1)
if not msg:
return False
if "quit" in msg and msg["quit"] is True:
# put the message back on the queue for others
self.global_queue.put_nowait(msg)
self.thread_stop = True
return True
def stop(self):
self.thread_stop = True
@abc.abstractmethod
def loop(self):
pass
def _cleanup(self):
"""Add code to subclass to do any cleanup"""
def run(self):
LOG.debug("Starting")
while not self._should_quit():
can_loop = self.loop()
if not can_loop:
self.stop()
self._cleanup()
APRSDThreadList().remove(self)
LOG.debug("Exiting")

View File

@ -0,0 +1,88 @@
import datetime
import logging
import time
import tracemalloc
from aprsd import client, messaging, packets, stats, utils
from aprsd.threads import APRSDThread, APRSDThreadList
LOG = logging.getLogger("APRSD")
class KeepAliveThread(APRSDThread):
cntr = 0
checker_time = datetime.datetime.now()
def __init__(self, config):
tracemalloc.start()
super().__init__("KeepAlive")
self.config = config
max_timeout = {"hours": 0.0, "minutes": 2, "seconds": 0}
self.max_delta = datetime.timedelta(**max_timeout)
def loop(self):
if self.cntr % 60 == 0:
tracker = messaging.MsgTrack()
stats_obj = stats.APRSDStats()
pl = packets.PacketList()
thread_list = APRSDThreadList()
now = datetime.datetime.now()
last_email = stats_obj.email_thread_time
if last_email:
email_thread_time = utils.strfdelta(now - last_email)
else:
email_thread_time = "N/A"
last_msg_time = utils.strfdelta(now - stats_obj.aprsis_keepalive)
current, peak = tracemalloc.get_traced_memory()
stats_obj.set_memory(current)
stats_obj.set_memory_peak(peak)
try:
login = self.config["aprsd"]["callsign"]
except KeyError:
login = self.config["ham"]["callsign"]
keepalive = (
"{} - Uptime {} RX:{} TX:{} Tracker:{} Msgs TX:{} RX:{} "
"Last:{} Email: {} - RAM Current:{} Peak:{} Threads:{}"
).format(
login,
utils.strfdelta(stats_obj.uptime),
pl.total_recv,
pl.total_tx,
len(tracker),
stats_obj.msgs_tx,
stats_obj.msgs_rx,
last_msg_time,
email_thread_time,
utils.human_size(current),
utils.human_size(peak),
len(thread_list),
)
LOG.info(keepalive)
# See if we should reset the aprs-is client
# Due to losing a keepalive from them
delta_dict = utils.parse_delta_str(last_msg_time)
delta = datetime.timedelta(**delta_dict)
if delta > self.max_delta:
# We haven't gotten a keepalive from aprs-is in a while
# reset the connection.a
if not client.KISSClient.is_enabled(self.config):
LOG.warning(f"Resetting connection to APRS-IS {delta}")
client.factory.create().reset()
# Check version every hour
delta = now - self.checker_time
if delta > datetime.timedelta(hours=1):
self.checker_time = now
level, msg = utils._check_version()
if level:
LOG.warning(msg)
self.cntr += 1
time.sleep(1)
return True

View File

@ -1,162 +1,15 @@
import abc import abc
import datetime
import logging import logging
import queue
import threading
import time import time
import tracemalloc
import aprslib import aprslib
from aprsd import client, messaging, packets, plugin, stats, utils from aprsd import client, messaging, packets, plugin, stats
from aprsd.threads import APRSDThread
LOG = logging.getLogger("APRSD") LOG = logging.getLogger("APRSD")
RX_THREAD = "RX"
EMAIL_THREAD = "Email"
rx_msg_queue = queue.Queue(maxsize=20)
msg_queues = {
"rx": rx_msg_queue,
}
class APRSDThreadList:
"""Singleton class that keeps track of application wide threads."""
_instance = None
threads_list = []
lock = None
def __new__(cls, *args, **kwargs):
if cls._instance is None:
cls._instance = super().__new__(cls)
cls.lock = threading.Lock()
cls.threads_list = []
return cls._instance
def add(self, thread_obj):
with self.lock:
self.threads_list.append(thread_obj)
def remove(self, thread_obj):
with self.lock:
self.threads_list.remove(thread_obj)
def stop_all(self):
"""Iterate over all threads and call stop on them."""
with self.lock:
for th in self.threads_list:
LOG.debug(f"Stopping Thread {th.name}")
th.stop()
def __len__(self):
with self.lock:
return len(self.threads_list)
class APRSDThread(threading.Thread, metaclass=abc.ABCMeta):
def __init__(self, name):
super().__init__(name=name)
self.thread_stop = False
APRSDThreadList().add(self)
def stop(self):
self.thread_stop = True
@abc.abstractmethod
def loop(self):
pass
def run(self):
LOG.debug("Starting")
while not self.thread_stop:
can_loop = self.loop()
if not can_loop:
self.stop()
APRSDThreadList().remove(self)
LOG.debug("Exiting")
class KeepAliveThread(APRSDThread):
cntr = 0
checker_time = datetime.datetime.now()
def __init__(self, config):
tracemalloc.start()
super().__init__("KeepAlive")
self.config = config
max_timeout = {"hours": 0.0, "minutes": 2, "seconds": 0}
self.max_delta = datetime.timedelta(**max_timeout)
def loop(self):
if self.cntr % 60 == 0:
tracker = messaging.MsgTrack()
stats_obj = stats.APRSDStats()
pl = packets.PacketList()
thread_list = APRSDThreadList()
now = datetime.datetime.now()
last_email = stats_obj.email_thread_time
if last_email:
email_thread_time = utils.strfdelta(now - last_email)
else:
email_thread_time = "N/A"
last_msg_time = utils.strfdelta(now - stats_obj.aprsis_keepalive)
current, peak = tracemalloc.get_traced_memory()
stats_obj.set_memory(current)
stats_obj.set_memory_peak(peak)
try:
login = self.config["aprs"]["login"]
except KeyError:
login = self.config["ham"]["callsign"]
keepalive = (
"{} - Uptime {} RX:{} TX:{} Tracker:{} Msgs TX:{} RX:{} "
"Last:{} Email: {} - RAM Current:{} Peak:{} Threads:{}"
).format(
login,
utils.strfdelta(stats_obj.uptime),
pl.total_recv,
pl.total_tx,
len(tracker),
stats_obj.msgs_tx,
stats_obj.msgs_rx,
last_msg_time,
email_thread_time,
utils.human_size(current),
utils.human_size(peak),
len(thread_list),
)
LOG.info(keepalive)
# See if we should reset the aprs-is client
# Due to losing a keepalive from them
delta_dict = utils.parse_delta_str(last_msg_time)
delta = datetime.timedelta(**delta_dict)
if delta > self.max_delta:
# We haven't gotten a keepalive from aprs-is in a while
# reset the connection.a
if not client.KISSClient.is_enabled(self.config):
LOG.warning("Resetting connection to APRS-IS.")
client.factory.create().reset()
# Check version every hour
delta = now - self.checker_time
if delta > datetime.timedelta(hours=1):
self.checker_time = now
level, msg = utils._check_version()
if level:
LOG.warning(msg)
self.cntr += 1
time.sleep(1)
return True
class APRSDRXThread(APRSDThread): class APRSDRXThread(APRSDThread):
def __init__(self, msg_queues, config): def __init__(self, msg_queues, config):
@ -186,7 +39,10 @@ class APRSDRXThread(APRSDThread):
self.process_packet, raw=False, blocking=False, self.process_packet, raw=False, blocking=False,
) )
except aprslib.exceptions.ConnectionDrop: except (
aprslib.exceptions.ConnectionDrop,
aprslib.exceptions.ConnectionError,
):
LOG.error("Connection dropped, reconnecting") LOG.error("Connection dropped, reconnecting")
time.sleep(5) time.sleep(5)
# Force the deletion of the client object connected to aprs # Force the deletion of the client object connected to aprs
@ -196,6 +52,12 @@ class APRSDRXThread(APRSDThread):
# Continue to loop # Continue to loop
return True return True
@abc.abstractmethod
def process_packet(self, *args, **kwargs):
pass
class APRSDPluginRXThread(APRSDRXThread):
def process_packet(self, *args, **kwargs): def process_packet(self, *args, **kwargs):
packet = self._client.decode_packet(*args, **kwargs) packet = self._client.decode_packet(*args, **kwargs)
thread = APRSDProcessPacketThread(packet=packet, config=self.config) thread = APRSDProcessPacketThread(packet=packet, config=self.config)
@ -239,7 +101,7 @@ class APRSDProcessPacketThread(APRSDThread):
# We don't put ack packets destined for us through the # We don't put ack packets destined for us through the
# plugins. # plugins.
if tocall == self.config["aprs"]["login"] and msg_response == "ack": if tocall == self.config["aprsd"]["callsign"] 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 # It's not an ACK for us, so lets run it through
@ -253,12 +115,12 @@ class APRSDProcessPacketThread(APRSDThread):
) )
# Only ack messages that were sent directly to us # Only ack messages that were sent directly to us
if tocall == self.config["aprs"]["login"]: if tocall == self.config["aprsd"]["callsign"]:
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["aprs"]["login"], self.config["aprsd"]["callsign"],
fromcall, fromcall,
msg_id=msg_id, msg_id=msg_id,
) )
@ -280,7 +142,7 @@ class APRSDProcessPacketThread(APRSDThread):
subreply.send() subreply.send()
else: else:
msg = messaging.TextMessage( msg = messaging.TextMessage(
self.config["aprs"]["login"], self.config["aprsd"]["callsign"],
fromcall, fromcall,
subreply, subreply,
) )
@ -300,7 +162,7 @@ class APRSDProcessPacketThread(APRSDThread):
LOG.debug(f"Sending '{reply}'") LOG.debug(f"Sending '{reply}'")
msg = messaging.TextMessage( msg = messaging.TextMessage(
self.config["aprs"]["login"], self.config["aprsd"]["callsign"],
fromcall, fromcall,
reply, reply,
) )
@ -308,10 +170,10 @@ class APRSDProcessPacketThread(APRSDThread):
# 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["aprs"]["login"] and not replied: if tocall == self.config["aprsd"]["callsign"] and not replied:
LOG.warning("Sending help!") LOG.warning("Sending help!")
msg = messaging.TextMessage( msg = messaging.TextMessage(
self.config["aprs"]["login"], self.config["aprsd"]["callsign"],
fromcall, fromcall,
"Unknown command! Send 'help' message for help", "Unknown command! Send 'help' message for help",
) )
@ -320,10 +182,10 @@ class APRSDProcessPacketThread(APRSDThread):
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["aprs"]["login"]: if tocall == self.config["aprsd"]["callsign"]:
reply = "A Plugin failed! try again?" reply = "A Plugin failed! try again?"
msg = messaging.TextMessage( msg = messaging.TextMessage(
self.config["aprs"]["login"], self.config["aprsd"]["callsign"],
fromcall, fromcall,
reply, reply,
) )

View File

@ -2,25 +2,17 @@
import collections import collections
import errno import errno
import functools
import os import os
import re import re
import threading
import update_checker import update_checker
import aprsd import aprsd
from .fuzzyclock import fuzzy # noqa: F401
def synchronized(wrapped): # Make these available by anyone importing
lock = threading.Lock() # aprsd.utils
from .ring_buffer import RingBuffer # noqa: F401
@functools.wraps(wrapped)
def _wrap(*args, **kwargs):
with lock:
return wrapped(*args, **kwargs)
return _wrap
def env(*vars, **kwargs): def env(*vars, **kwargs):
@ -129,42 +121,3 @@ def parse_delta_str(s):
else: else:
m = re.match(r"(?P<hours>\d+):(?P<minutes>\d+):(?P<seconds>\d[\.\d+]*)", s) m = re.match(r"(?P<hours>\d+):(?P<minutes>\d+):(?P<seconds>\d[\.\d+]*)", s)
return {key: float(val) for key, val in m.groupdict().items()} return {key: float(val) for key, val in m.groupdict().items()}
class RingBuffer:
"""class that implements a not-yet-full buffer"""
def __init__(self, size_max):
self.max = size_max
self.data = []
class __Full:
"""class that implements a full buffer"""
def append(self, x):
"""Append an element overwriting the oldest one."""
self.data[self.cur] = x
self.cur = (self.cur + 1) % self.max
def get(self):
"""return list of elements in correct order"""
return self.data[self.cur :] + self.data[: self.cur]
def __len__(self):
return len(self.data)
def append(self, x):
"""append an element at the end of the buffer"""
self.data.append(x)
if len(self.data) == self.max:
self.cur = 0
# Permanently change self's class from non-full to full
self.__class__ = self.__Full
def get(self):
"""Return a list of elements from the oldest to the newest."""
return self.data
def __len__(self):
return len(self.data)

View File

@ -0,0 +1,37 @@
class RingBuffer:
"""class that implements a not-yet-full buffer"""
def __init__(self, size_max):
self.max = size_max
self.data = []
class __Full:
"""class that implements a full buffer"""
def append(self, x):
"""Append an element overwriting the oldest one."""
self.data[self.cur] = x
self.cur = (self.cur + 1) % self.max
def get(self):
"""return list of elements in correct order"""
return self.data[self.cur :] + self.data[: self.cur]
def __len__(self):
return len(self.data)
def append(self, x):
"""append an element at the end of the buffer"""
self.data.append(x)
if len(self.data) == self.max:
self.cur = 0
# Permanently change self's class from non-full to full
self.__class__ = self.__Full
def get(self):
"""Return a list of elements from the oldest to the newest."""
return self.data
def __len__(self):
return len(self.data)

0
aprsd/web/__init__.py Normal file
View File

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

View File

Before

Width:  |  Height:  |  Size: 52 KiB

After

Width:  |  Height:  |  Size: 52 KiB

View File

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 48 KiB

View File

Before

Width:  |  Height:  |  Size: 52 KiB

After

Width:  |  Height:  |  Size: 52 KiB

View File

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 48 KiB

View File

Before

Width:  |  Height:  |  Size: 40 KiB

After

Width:  |  Height:  |  Size: 40 KiB

View File

@ -0,0 +1,94 @@
body {
background: #eeeeee;
margin: 2em;
text-align: center;
font-family: system-ui, sans-serif;
}
footer {
padding: 2em;
text-align: center;
height: 10vh;
}
.ui.segment {
background: #eeeeee;
}
ul.list {
list-style-type: disc;
}
ul.list li {
list-style-position: outside;
}
#left {
margin-right: 2px;
height: 300px;
}
#right {
height: 300px;
}
#center {
height: 300px;
}
#title {
font-size: 4em;
}
#version{
font-size: .5em;
}
#uptime, #aprsis {
font-size: 1em;
}
#callsign {
font-size: 1.4em;
color: #00F;
padding-top: 8px;
margin:10px;
}
#title_rx {
background-color: darkseagreen;
text-align: left;
}
#title_tx {
background-color: lightcoral;
text-align: left;
}
.aprsd_1 {
background-image: url(/static/images/aprs-symbols-16-0.png);
background-repeat: no-repeat;
background-position: -160px -48px;
width: 16px;
height: 16px;
}
#msgsTabsDiv .ui.tab {
margin:0px;
padding:0px;
display: block;
}
#msgsTabsDiv .header, .tiny.text, .content, .break,
.thumbs.down.outline.icon,
.phone.volume.icon
{
display: inline-block;
float: left;
position: relative;
}
#msgsTabsDiv .tiny.text {
width:100px;
}
#msgsTabsDiv .tiny.header {
width:100px;
text-align: left;
}
#msgsTabsDiv .break {
margin: 2px;
text-align: left;
}

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,41 @@
* {box-sizing: border-box}
/* Style the tab */
.tab {
border: 1px solid #ccc;
background-color: #f1f1f1;
height: 450px;
}
/* Style the buttons inside the tab */
.tab div {
display: block;
background-color: inherit;
color: black;
padding: 10px;
width: 100%;
border: none;
outline: none;
text-align: left;
cursor: pointer;
transition: 0.3s;
font-size: 17px;
}
/* Change background color of buttons on hover */
.tab div:hover {
background-color: #ddd;
}
/* Create an active/current "tab button" class */
.tab div.active {
background-color: #ccc;
}
/* Style the tab content */
.tabcontent {
border: 1px solid #ccc;
height: 450px;
overflow-y: scroll;
background-color: white;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

View File

@ -0,0 +1,44 @@
function aprs_img(item, x_offset, y_offset) {
var x = x_offset * -16;
if (y_offset > 5) {
y_offset = 5;
}
var y = y_offset * -16;
var loc = x + 'px '+ y + 'px'
item.css('background-position', loc);
}
function show_aprs_icon(item, symbol) {
var offset = ord(symbol) - 33;
var col = Math.floor(offset / 16);
var row = offset % 16;
//console.log("'" + symbol+"' off: "+offset+" row: "+ row + " col: " + col)
aprs_img(item, row, col);
}
function ord(str){return str.charCodeAt(0);}
function update_stats( data ) {
$("#version").text( data["stats"]["aprsd"]["version"] );
$("#aprs_connection").html( data["aprs_connection"] );
$("#uptime").text( "uptime: " + data["stats"]["aprsd"]["uptime"] );
short_time = data["time"].split(/\s(.+)/)[1];
}
function start_update() {
(function statsworker() {
$.ajax({
url: "/stats",
type: 'GET',
dataType: 'json',
success: function(data) {
update_stats(data);
},
complete: function() {
setTimeout(statsworker, 10000);
}
});
})();
}

View File

@ -0,0 +1,215 @@
var cleared = false;
var callsign_list = {};
var message_list = {};
function size_dict(d){c=0; for (i in d) ++c; return c}
function init_chat() {
const socket = io("/sendmsg");
socket.on('connect', function () {
console.log("Connected to socketio");
});
socket.on('connected', function(msg) {
console.log("Connected!");
console.log(msg);
});
socket.on("sent", function(msg) {
if (cleared == false) {
var msgsdiv = $("#msgsTabsDiv");
msgsdiv.html('')
cleared = true
}
sent_msg(msg);
});
socket.on("ack", function(msg) {
update_msg(msg);
});
socket.on("new", function(msg) {
if (cleared == false) {
var msgsdiv = $("#msgsTabsDiv");
msgsdiv.html('')
cleared = true
}
from_msg(msg);
});
$("#sendform").submit(function(event) {
event.preventDefault();
msg = {'to': $('#to_call').val(),
'message': $('#message').val(),
}
socket.emit("send", msg);
$('#message').val('');
});
}
function add_callsign(callsign) {
/* Ensure a callsign exists in the left hand nav */
if (callsign in callsign_list) {
return false
}
var callsignTabs = $("#callsignTabs");
tab_name = tab_string(callsign);
tab_content = tab_content_name(callsign);
divname = content_divname(callsign);
item_html = '<div class="tablinks" id="'+tab_name+'" onclick="openCallsign(event, \''+callsign+'\');">'+callsign+'</div>';
callsignTabs.append(item_html);
callsign_list[callsign] = true;
return true
}
function append_message(callsign, msg, msg_html) {
new_callsign = false
if (!message_list.hasOwnProperty(callsign)) {
message_list[callsign] = new Array();
}
message_list[callsign].push(msg);
// Find the right div to place the html
new_callsign = add_callsign(callsign);
append_message_html(callsign, msg_html, new_callsign);
if (new_callsign) {
//click on the new tab
click_div = '#'+tab_string(callsign);
$(click_div).click();
}
}
function tab_string(callsign) {
return "msgs"+callsign;
}
function tab_content_name(callsign) {
return tab_string(callsign)+"Content";
}
function content_divname(callsign) {
return "#"+tab_content_name(callsign);
}
function append_message_html(callsign, msg_html, new_callsign) {
var msgsTabs = $('#msgsTabsDiv');
divname_str = tab_content_name(callsign);
divname = content_divname(callsign);
if (new_callsign) {
// we have to add a new DIV
msg_div_html = '<div class="tabcontent" id="'+divname_str+'" style="height:450px;">'+msg_html+'</div>';
msgsTabs.append(msg_div_html);
} else {
var msgDiv = $(divname);
msgDiv.append(msg_html);
}
$(divname).animate({scrollTop: $(divname)[0].scrollHeight}, "slow");
}
function create_message_html(time, from, to, message, ack) {
msg_html = '<div class="item">';
msg_html += '<div class="tiny text">'+time+'</div>';
msg_html += '<div class="middle aligned content">';
msg_html += '<div class="tiny red header">'+from+'</div>';
if (ack) {
msg_html += '<i class="thumbs down outline icon" id="' + ack_id + '" data-content="Waiting for ACK"></i>';
} else {
msg_html += '<i class="phone volume icon" data-content="Recieved Message"></i>';
}
msg_html += '<div class="middle aligned content">>&nbsp;&nbsp;&nbsp;</div>';
msg_html += '</div>';
msg_html += '<div class="middle aligned content">'+message+'</div>';
msg_html += '</div><br>';
return msg_html
}
function sent_msg(msg) {
var msgsdiv = $("#sendMsgsDiv");
ts_str = msg["ts"].toString();
ts = ts_str.split(".")[0]*1000;
id = ts_str.split('.')[0]
ack_id = "ack_" + id
var d = new Date(ts).toLocaleDateString("en-US")
var t = new Date(ts).toLocaleTimeString("en-US")
msg_html = create_message_html(t, msg['from'], msg['to'], msg['message'], ack_id);
append_message(msg['to'], msg, msg_html);
}
function from_msg(msg) {
var msgsdiv = $("#sendMsgsDiv");
// We have an existing entry
ts_str = msg["ts"].toString();
ts = ts_str.split(".")[0]*1000;
id = ts_str.split('.')[0]
ack_id = "ack_" + id
var d = new Date(ts).toLocaleDateString("en-US")
var t = new Date(ts).toLocaleTimeString("en-US")
from = msg['from']
msg_html = create_message_html(t, from, false, msg['message'], false);
append_message(from, msg, msg_html);
}
function update_msg(msg) {
var msgsdiv = $("#sendMsgsDiv");
// We have an existing entry
ts_str = msg["ts"].toString();
id = ts_str.split('.')[0]
pretty_id = "pretty_" + id
loader_id = "loader_" + id
ack_id = "ack_" + id
span_id = "span_" + id
if (msg['ack'] == true) {
var loader_div = $('#' + loader_id);
var ack_div = $('#' + ack_id);
loader_div.removeClass('ui active inline loader');
loader_div.addClass('ui disabled loader');
ack_div.removeClass('thumbs up outline icon');
ack_div.addClass('thumbs up outline icon');
}
$('.ui.accordion').accordion('refresh');
}
function callsign_select(callsign) {
var tocall = $("#to_call");
tocall.val(callsign);
}
function reset_Tabs() {
tabcontent = document.getElementsByClassName("tabcontent");
for (i = 0; i < tabcontent.length; i++) {
tabcontent[i].style.display = "none";
}
}
function openCallsign(evt, callsign) {
var i, tabcontent, tablinks;
tab_content = tab_content_name(callsign);
tabcontent = document.getElementsByClassName("tabcontent");
for (i = 0; i < tabcontent.length; i++) {
tabcontent[i].style.display = "none";
}
tablinks = document.getElementsByClassName("tablinks");
for (i = 0; i < tablinks.length; i++) {
tablinks[i].className = tablinks[i].className.replace(" active", "");
}
document.getElementById(tab_content).style.display = "block";
evt.target.className += " active";
callsign_select(callsign);
}

View File

@ -0,0 +1,28 @@
function openTab(evt, tabName) {
// Declare all variables
var i, tabcontent, tablinks;
if (typeof tabName == 'undefined') {
return
}
// Get all elements with class="tabcontent" and hide them
tabcontent = document.getElementsByClassName("tabcontent");
for (i = 0; i < tabcontent.length; i++) {
tabcontent[i].style.display = "none";
}
// Get all elements with class="tablinks" and remove the class "active"
tablinks = document.getElementsByClassName("tablinks");
for (i = 0; i < tablinks.length; i++) {
tablinks[i].className = tablinks[i].className.replace(" active", "");
}
// Show the current tab, and add an "active" class to the button that opened the tab
document.getElementById(tabName).style.display = "block";
if (typeof evt.currentTarget == 'undefined') {
return
} else {
evt.currentTarget.className += " active";
}
}

View File

@ -0,0 +1,57 @@
/* Root element */
.json-document {
padding: 1em 2em;
}
/* Syntax highlighting for JSON objects */
ul.json-dict, ol.json-array {
list-style-type: none;
margin: 0 0 0 1px;
border-left: 1px dotted #ccc;
padding-left: 2em;
}
.json-string {
color: #0B7500;
}
.json-literal {
color: #1A01CC;
font-weight: bold;
}
/* Toggle button */
a.json-toggle {
position: relative;
color: inherit;
text-decoration: none;
}
a.json-toggle:focus {
outline: none;
}
a.json-toggle:before {
font-size: 1.1em;
color: #c0c0c0;
content: "\25BC"; /* down arrow */
position: absolute;
display: inline-block;
width: 1em;
text-align: center;
line-height: 1em;
left: -1.2em;
}
a.json-toggle:hover:before {
color: #aaa;
}
a.json-toggle.collapsed:before {
/* Use rotated down arrow, prevents right arrow appearing smaller than down arrow in some browsers */
transform: rotate(-90deg);
}
/* Collapsable placeholder links */
a.json-placeholder {
color: #aaa;
padding: 0 1em;
text-decoration: none;
}
a.json-placeholder:hover {
text-decoration: underline;
}

View File

@ -0,0 +1,158 @@
/**
* jQuery json-viewer
* @author: Alexandre Bodelot <alexandre.bodelot@gmail.com>
* @link: https://github.com/abodelot/jquery.json-viewer
*/
(function($) {
/**
* Check if arg is either an array with at least 1 element, or a dict with at least 1 key
* @return boolean
*/
function isCollapsable(arg) {
return arg instanceof Object && Object.keys(arg).length > 0;
}
/**
* Check if a string represents a valid url
* @return boolean
*/
function isUrl(string) {
var urlRegexp = /^(https?:\/\/|ftps?:\/\/)?([a-z0-9%-]+\.){1,}([a-z0-9-]+)?(:(\d{1,5}))?(\/([a-z0-9\-._~:/?#[\]@!$&'()*+,;=%]+)?)?$/i;
return urlRegexp.test(string);
}
/**
* Transform a json object into html representation
* @return string
*/
function json2html(json, options) {
var html = '';
if (typeof json === 'string') {
// Escape tags and quotes
json = json
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/'/g, '&apos;')
.replace(/"/g, '&quot;');
if (options.withLinks && isUrl(json)) {
html += '<a href="' + json + '" class="json-string" target="_blank">' + json + '</a>';
} else {
// Escape double quotes in the rendered non-URL string.
json = json.replace(/&quot;/g, '\\&quot;');
html += '<span class="json-string">"' + json + '"</span>';
}
} else if (typeof json === 'number') {
html += '<span class="json-literal">' + json + '</span>';
} else if (typeof json === 'boolean') {
html += '<span class="json-literal">' + json + '</span>';
} else if (json === null) {
html += '<span class="json-literal">null</span>';
} else if (json instanceof Array) {
if (json.length > 0) {
html += '[<ol class="json-array">';
for (var i = 0; i < json.length; ++i) {
html += '<li>';
// Add toggle button if item is collapsable
if (isCollapsable(json[i])) {
html += '<a href class="json-toggle"></a>';
}
html += json2html(json[i], options);
// Add comma if item is not last
if (i < json.length - 1) {
html += ',';
}
html += '</li>';
}
html += '</ol>]';
} else {
html += '[]';
}
} else if (typeof json === 'object') {
var keyCount = Object.keys(json).length;
if (keyCount > 0) {
html += '{<ul class="json-dict">';
for (var key in json) {
if (Object.prototype.hasOwnProperty.call(json, key)) {
html += '<li>';
var keyRepr = options.withQuotes ?
'<span class="json-string">"' + key + '"</span>' : key;
// Add toggle button if item is collapsable
if (isCollapsable(json[key])) {
html += '<a href class="json-toggle">' + keyRepr + '</a>';
} else {
html += keyRepr;
}
html += ': ' + json2html(json[key], options);
// Add comma if item is not last
if (--keyCount > 0) {
html += ',';
}
html += '</li>';
}
}
html += '</ul>}';
} else {
html += '{}';
}
}
return html;
}
/**
* jQuery plugin method
* @param json: a javascript object
* @param options: an optional options hash
*/
$.fn.jsonViewer = function(json, options) {
// Merge user options with default options
options = Object.assign({}, {
collapsed: false,
rootCollapsable: true,
withQuotes: false,
withLinks: true
}, options);
// jQuery chaining
return this.each(function() {
// Transform to HTML
var html = json2html(json, options);
if (options.rootCollapsable && isCollapsable(json)) {
html = '<a href class="json-toggle"></a>' + html;
}
// Insert HTML in target DOM element
$(this).html(html);
$(this).addClass('json-document');
// Bind click on toggle buttons
$(this).off('click');
$(this).on('click', 'a.json-toggle', function() {
var target = $(this).toggleClass('collapsed').siblings('ul.json-dict, ol.json-array');
target.toggle();
if (target.is(':visible')) {
target.siblings('.json-placeholder').remove();
} else {
var count = target.children('li').length;
var placeholder = count + (count > 1 ? ' items' : ' item');
target.after('<a href class="json-placeholder">' + placeholder + '</a>');
}
return false;
});
// Simulate click on toggle button when placeholder is clicked
$(this).on('click', 'a.json-placeholder', function() {
$(this).siblings('a.json-toggle').click();
return false;
});
if (options.collapsed == true) {
// Trigger click to collapse all nodes
$(this).find('a.json-toggle').click();
}
});
};
})(jQuery);

View File

@ -0,0 +1,86 @@
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1">
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
<link rel="stylesheet" href="https://ajax.googleapis.com/ajax/libs/jqueryui/1.12.1/themes/smoothness/jquery-ui.css">
<script src="https://ajax.googleapis.com/ajax/libs/jqueryui/1.12.1/jquery-ui.min.js"></script>
<script src="https://cdn.socket.io/4.1.2/socket.io.min.js" integrity="sha384-toS6mmwu70G0fw54EGlWWeA4z3dyJ+dlXBtSURSKN4vyRFOcxd3Bzjj/AoOwY+Rg" crossorigin="anonymous"></script>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/semantic-ui@2.4.2/dist/semantic.min.css">
<script src="https://cdn.jsdelivr.net/npm/semantic-ui@2.4.2/dist/semantic.min.js"></script>
<link rel="stylesheet" href="/static/css/index.css">
<link rel="stylesheet" href="/static/css/tabs.css">
<script src="/static/js/main.js"></script>
<script src="/static/js/send-message.js"></script>
<script type="text/javascript">
var initial_stats = {{ initial_stats|tojson|safe }};
var memory_chart = null
var message_chart = null
$(document).ready(function() {
console.log(initial_stats);
start_update();
init_chat();
reset_Tabs();
});
</script>
</head>
<body>
<div class='ui text container'>
<h1 class='ui dividing header'>APRSD WebChat {{ version }}</h1>
</div>
<div class='ui grid text container'>
<div class='left floated ten wide column'>
<span style='color: green'>{{ callsign }}</span>
connected to
<span style='color: blue' id='aprs_connection'>{{ aprs_connection|safe }}</span>
</div>
<div class='right floated four wide column'>
<span id='uptime'>NONE</span>
</div>
</div>
<div class="ui container">
<h3 class="ui dividing header">Send Message</h3>
<div id="sendMsgDiv" class="ui mini text">
<form id="sendform" name="sendmsg" action="">
<div class="ui corner labeled input">
<label for="to_call" class="ui label">Callsign</label>
<input type="text" name="to_call" id="to_call" placeholder="To Callsign" size="11" maxlength="9">
<div class="ui corner label">
<i class="asterisk icon"></i>
</div>
</div>
<div class="ui labeled input">
<label for="message" class="ui label">Message</label>
<input type="text" name="message" id="message" size="40" maxlength="40">
</div>
<input type="submit" name="submit" class="ui button" id="send_msg" value="Send" />
</form>
</div>
</div>
<div class="ui grid">
<div class="three wide column">
<div class="tab" id="callsignTabs">
</div>
</div>
<div class="ten wide column ui raised segment" id="msgsTabsDiv" style="height:450px;padding:0px;">
&nbsp;
</div>
</div>
</div>
<div class="ui text container" style="padding-top: 40px">
<a href="https://badge.fury.io/py/aprsd"><img src="https://badge.fury.io/py/aprsd.svg" alt="PyPI version" height="18"></a>
<a href="https://github.com/craigerl/aprsd"><img src="https://img.shields.io/badge/Made%20with-Python-1f425f.svg" height="18"></a>
</div>
</body>
</html>

View File

@ -10,3 +10,5 @@ pip-tools
pytest pytest
pytest-cov pytest-cov
gray gray
pip==22.0.4
pip-tools==5.4.0

View File

@ -1,257 +1,99 @@
# #
# This file is autogenerated by pip-compile with python 3.8 # This file is autogenerated by pip-compile
# To update, run: # To update, run:
# #
# pip-compile dev-requirements.in # pip-compile dev-requirements.in
# #
add-trailing-comma==2.1.0 add-trailing-comma==2.3.0 # via gray
# via gray alabaster==0.7.12 # via sphinx
alabaster==0.7.12 attrs==22.1.0 # via jsonschema, pytest
# via sphinx autoflake==1.7.7 # via gray
appdirs==1.4.4 babel==2.11.0 # via sphinx
# via black black==22.10.0 # via gray
attrs==21.2.0 bleach==5.0.1 # via readme-renderer
# via certifi==2022.9.24 # via requests
# jsonschema cfgv==3.3.1 # via pre-commit
# pytest charset-normalizer==2.1.1 # via requests
autoflake==1.4 click==8.1.3 # via black, pip-tools
# via gray colorlog==6.7.0 # via prettylog
babel==2.9.1 commonmark==0.9.1 # via rich
# via sphinx configargparse==1.5.3 # via gray
backports-entry-points-selectable==1.1.0 coverage[toml]==6.5.0 # via pytest-cov
# via virtualenv distlib==0.3.6 # via virtualenv
black==21.7b0 docutils==0.19 # via readme-renderer, sphinx
# via gray exceptiongroup==1.0.4 # via pytest
bleach==4.1.0 fast-json==0.3.2 # via prettylog
# via readme-renderer filelock==3.8.0 # via tox, virtualenv
build==0.9.0 fixit==0.1.4 # via gray
# via pip-tools flake8==5.0.4 # via -r dev-requirements.in, fixit, pep8-naming
certifi==2021.5.30 gray==0.12.0 # via -r dev-requirements.in
# via requests identify==2.5.9 # via pre-commit
cfgv==3.3.1 idna==3.4 # via requests
# via pre-commit imagesize==1.4.1 # via sphinx
charset-normalizer==2.0.4 importlib-metadata==5.0.0 # via keyring, sphinx, twine
# via requests importlib-resources==5.10.0 # via fixit, jsonschema
click==8.0.1 iniconfig==1.1.1 # via pytest
# via isort==5.10.1 # via -r dev-requirements.in, gray
# black jaraco.classes==3.2.3 # via keyring
# pip-tools jinja2==3.1.2 # via sphinx
colorama==0.4.4 jsonschema==4.17.1 # via fixit
# via twine keyring==23.11.0 # via twine
colorlog==6.4.1 libcst==0.4.9 # via fixit
# via prettylog markupsafe==2.1.1 # via jinja2
configargparse==1.5.2 mccabe==0.7.0 # via flake8
# via gray more-itertools==9.0.0 # via jaraco.classes
coverage==5.5 mypy-extensions==0.4.3 # via black, mypy, typing-inspect
# via pytest-cov mypy==0.991 # via -r dev-requirements.in
distlib==0.3.2 nodeenv==1.7.0 # via pre-commit
# via virtualenv packaging==21.3 # via pytest, sphinx, tox
docutils==0.17.1 pathspec==0.10.2 # via black
# via pep8-naming==0.13.2 # via -r dev-requirements.in
# readme-renderer pip-tools==5.4.0 # via -r dev-requirements.in
# sphinx pkginfo==1.8.3 # via twine
fast-json==0.3.2 pkgutil-resolve-name==1.3.10 # via jsonschema
# via prettylog platformdirs==2.5.4 # via black, virtualenv
filelock==3.0.12 pluggy==1.0.0 # via pytest, tox
# via pre-commit==2.20.0 # via -r dev-requirements.in
# tox prettylog==0.3.0 # via gray
# virtualenv py==1.11.0 # via tox
fixit==0.1.4 pycodestyle==2.9.1 # via flake8
# via gray pyflakes==2.5.0 # via autoflake, flake8
flake8==3.9.2 pygments==2.13.0 # via readme-renderer, rich, sphinx
# via pyparsing==3.0.9 # via packaging
# -r dev-requirements.in pyrsistent==0.19.2 # via jsonschema
# fixit pytest-cov==4.0.0 # via -r dev-requirements.in
# flake8-polyfill pytest==7.2.0 # via -r dev-requirements.in, pytest-cov
# pep8-naming pytz==2022.6 # via babel
flake8-polyfill==1.0.2 pyupgrade==3.2.2 # via gray
# via pep8-naming pyyaml==6.0 # via fixit, libcst, pre-commit
gray==0.10.1 readme-renderer==37.3 # via twine
# via -r dev-requirements.in requests-toolbelt==0.10.1 # via twine
identify==2.2.13 requests==2.28.1 # via requests-toolbelt, sphinx, twine
# via pre-commit rfc3986==2.0.0 # via twine
idna==3.2 rich==12.6.0 # via twine
# via requests six==1.16.0 # via bleach, pip-tools, tox
imagesize==1.2.0 snowballstemmer==2.2.0 # via sphinx
# via sphinx sphinx==5.3.0 # via -r dev-requirements.in
importlib-metadata==4.7.1 sphinxcontrib-applehelp==1.0.2 # via sphinx
# via sphinxcontrib-devhelp==1.0.2 # via sphinx
# keyring sphinxcontrib-htmlhelp==2.0.0 # via sphinx
# twine sphinxcontrib-jsmath==1.0.1 # via sphinx
importlib-resources==5.2.2 sphinxcontrib-qthelp==1.0.3 # via sphinx
# via fixit sphinxcontrib-serializinghtml==1.1.5 # via sphinx
iniconfig==1.1.1 tokenize-rt==5.0.0 # via add-trailing-comma, pyupgrade
# via pytest toml==0.10.2 # via pre-commit
isort==5.9.3 tomli==2.0.1 # via autoflake, black, coverage, mypy, pytest, tox
# via tox==3.27.1 # via -r dev-requirements.in
# -r dev-requirements.in twine==4.0.1 # via -r dev-requirements.in
# gray typing-extensions==4.4.0 # via black, libcst, mypy, rich, typing-inspect
jinja2==3.0.1 typing-inspect==0.8.0 # via libcst
# via sphinx ujson==5.5.0 # via fast-json
jsonschema==3.2.0 unify==0.5 # via gray
# via fixit untokenize==0.1.1 # via unify
keyring==23.1.0 urllib3==1.26.12 # via requests, twine
# via twine virtualenv==20.16.7 # via pre-commit, tox
libcst==0.3.20 webencodings==0.5.1 # via bleach
# via fixit zipp==3.10.0 # via importlib-metadata, importlib-resources
markupsafe==2.0.1
# via jinja2
mccabe==0.6.1
# via flake8
mypy==0.910
# via -r dev-requirements.in
mypy-extensions==0.4.3
# via
# black
# mypy
# typing-inspect
nodeenv==1.6.0
# via pre-commit
packaging==21.0
# via
# bleach
# build
# pytest
# sphinx
# tox
pathspec==0.9.0
# via black
pep517==0.11.0
# via build
pep8-naming==0.12.1
# via -r dev-requirements.in
pip-tools==6.9.0
# via -r dev-requirements.in
pkginfo==1.7.1
# via twine
platformdirs==2.2.0
# via virtualenv
pluggy==1.0.0
# via
# pytest
# tox
pre-commit==2.14.0
# via -r dev-requirements.in
prettylog==0.3.0
# via gray
py==1.10.0
# via
# pytest
# tox
pycodestyle==2.7.0
# via flake8
pyflakes==2.3.1
# via
# autoflake
# flake8
pygments==2.10.0
# via
# readme-renderer
# sphinx
pyparsing==2.4.7
# via packaging
pyrsistent==0.18.0
# via jsonschema
pytest==6.2.5
# via
# -r dev-requirements.in
# pytest-cov
pytest-cov==2.12.1
# via -r dev-requirements.in
pytz==2021.1
# via babel
pyupgrade==2.24.0
# via gray
pyyaml==5.4.1
# via
# fixit
# libcst
# pre-commit
readme-renderer==29.0
# via twine
regex==2021.8.27
# via black
requests==2.26.0
# via
# requests-toolbelt
# sphinx
# twine
requests-toolbelt==0.9.1
# via twine
rfc3986==1.5.0
# via twine
six==1.16.0
# via
# bleach
# jsonschema
# readme-renderer
# tox
# virtualenv
snowballstemmer==2.1.0
# via sphinx
sphinx==4.1.2
# via -r dev-requirements.in
sphinxcontrib-applehelp==1.0.2
# via sphinx
sphinxcontrib-devhelp==1.0.2
# via sphinx
sphinxcontrib-htmlhelp==2.0.0
# via sphinx
sphinxcontrib-jsmath==1.0.1
# via sphinx
sphinxcontrib-qthelp==1.0.3
# via sphinx
sphinxcontrib-serializinghtml==1.1.5
# via sphinx
tokenize-rt==4.1.0
# via
# add-trailing-comma
# pyupgrade
toml==0.10.2
# via
# mypy
# pre-commit
# pytest
# pytest-cov
# tox
tomli==1.2.1
# via
# black
# build
# pep517
tox==3.24.3
# via -r dev-requirements.in
tqdm==4.62.2
# via twine
twine==3.4.2
# via -r dev-requirements.in
typing-extensions==3.10.0.0
# via
# libcst
# mypy
# typing-inspect
typing-inspect==0.7.1
# via libcst
ujson==4.1.0
# via fast-json
unify==0.5
# via gray
untokenize==0.1.1
# via unify
urllib3==1.26.6
# via requests
virtualenv==20.7.2
# via
# pre-commit
# tox
webencodings==0.5.1
# via bleach
wheel==0.37.0
# via pip-tools
zipp==3.5.0
# via
# importlib-metadata
# importlib-resources
# The following packages are considered to be unsafe in a requirements file: # The following packages are considered to be unsafe in a requirements file:
# pip # pip

View File

@ -1,20 +1,19 @@
aioax25>=0.0.10
aprslib>=0.7.0 aprslib>=0.7.0
click click
click-completion click-completion
flask flask==2.1.2
werkzeug==2.1.2
flask-classful flask-classful
flask-httpauth flask-httpauth
imapclient imapclient
opencage
pluggy pluggy
pbr pbr
pyyaml pyyaml
# Allowing a newer version can lead to a conflict with # Allowing a newer version can lead to a conflict with
# requests. # requests.
py3-validate-email py3-validate-email
pytz
requests requests
pytz
six six
thesmuggler thesmuggler
update_checker update_checker
@ -24,3 +23,7 @@ tabulate
rich rich
# For the list-plugins pypi.org search scraping # For the list-plugins pypi.org search scraping
beautifulsoup4 beautifulsoup4
wrapt
# kiss3 uses attrs
kiss3
attrs==22.1.0

View File

@ -1,130 +1,55 @@
# #
# This file is autogenerated by pip-compile with python 3.8 # This file is autogenerated by pip-compile
# To update, run: # To update, run:
# #
# pip-compile requirements.in # pip-compile requirements.in
# #
aioax25==0.0.10 aprslib==0.7.2 # via -r requirements.in
# via -r requirements.in attrs==22.1.0 # via -r requirements.in, ax253, kiss3
aprslib==0.7.0 ax253==0.1.5.post1 # via kiss3
# via -r requirements.in beautifulsoup4==4.11.1 # via -r requirements.in
backoff==1.11.1 bidict==0.22.0 # via python-socketio
# via opencage bitarray==2.6.0 # via ax253, kiss3
beautifulsoup4==4.10.0 certifi==2022.9.24 # via requests
# via -r requirements.in charset-normalizer==2.1.1 # via requests
bidict==0.21.2 click-completion==0.5.2 # via -r requirements.in
# via python-socketio click==8.1.3 # via -r requirements.in, click-completion, flask
certifi==2021.5.30 commonmark==0.9.1 # via rich
# via requests dnspython==2.2.1 # via eventlet, py3-validate-email
cffi==1.14.6 eventlet==0.33.2 # via -r requirements.in
# via cryptography filelock==3.8.0 # via py3-validate-email
charset-normalizer==2.0.4 flask-classful==0.14.2 # via -r requirements.in
# via requests flask-httpauth==4.7.0 # via -r requirements.in
click==8.0.1 flask-socketio==5.3.2 # via -r requirements.in
# via flask==2.1.2 # via -r requirements.in, flask-classful, flask-httpauth, flask-socketio
# -r requirements.in greenlet==2.0.1 # via eventlet
# click-completion idna==3.4 # via py3-validate-email, requests
# flask imapclient==2.3.1 # via -r requirements.in
click-completion==0.5.2 importlib-metadata==5.0.0 # via ax253, flask, kiss3
# via -r requirements.in itsdangerous==2.1.2 # via flask
colorama==0.4.4 jinja2==3.1.2 # via click-completion, flask
# via rich kiss3==8.0.0 # via -r requirements.in
commonmark==0.9.1 markupsafe==2.1.1 # via jinja2
# via rich pbr==5.11.0 # via -r requirements.in
contexter==0.1.4 pluggy==1.0.0 # via -r requirements.in
# via signalslot py3-validate-email==1.0.5.post1 # via -r requirements.in
cryptography==3.4.7 pygments==2.13.0 # via rich
# via pyopenssl pyserial-asyncio==0.6 # via kiss3
dnspython==2.1.0 pyserial==3.5 # via pyserial-asyncio
# via python-engineio==4.3.4 # via python-socketio
# eventlet python-socketio==5.7.2 # via flask-socketio
# py3-validate-email pytz==2022.6 # via -r requirements.in
eventlet==0.33.1 pyyaml==6.0 # via -r requirements.in
# via -r requirements.in requests==2.28.1 # via -r requirements.in, update-checker
filelock==3.0.12 rich==12.6.0 # via -r requirements.in
# via py3-validate-email shellingham==1.5.0 # via click-completion
flask==2.0.1 six==1.16.0 # via -r requirements.in, click-completion, eventlet, imapclient
# via soupsieve==2.3.2.post1 # via beautifulsoup4
# -r requirements.in tabulate==0.9.0 # via -r requirements.in
# flask-classful thesmuggler==1.0.1 # via -r requirements.in
# flask-httpauth typing-extensions==4.4.0 # via rich
# flask-socketio update_checker==0.18.0 # via -r requirements.in
flask-classful==0.14.2 urllib3==1.26.12 # via requests
# via -r requirements.in werkzeug==2.1.2 # via -r requirements.in, flask
flask-httpauth==4.4.0 wrapt==1.14.1 # via -r requirements.in
# via -r requirements.in zipp==3.10.0 # via importlib-metadata
flask-socketio==5.1.1
# via -r requirements.in
greenlet==1.1.1
# via eventlet
idna==3.2
# via
# py3-validate-email
# requests
imapclient==2.2.0
# via -r requirements.in
itsdangerous==2.0.1
# via flask
jinja2==3.0.1
# via
# click-completion
# flask
markupsafe==2.0.1
# via jinja2
opencage==2.0.0
# via -r requirements.in
pbr==5.6.0
# via -r requirements.in
pluggy==1.0.0
# via -r requirements.in
py3-validate-email==1.0.1
# via -r requirements.in
pycparser==2.20
# via cffi
pygments==2.10.0
# via rich
pyopenssl==20.0.1
# via opencage
pyserial==3.5
# via aioax25
python-engineio==4.2.1
# via python-socketio
python-socketio==5.4.0
# via flask-socketio
pytz==2021.1
# via -r requirements.in
pyyaml==5.4.1
# via -r requirements.in
requests==2.26.0
# via
# -r requirements.in
# opencage
# update-checker
rich==10.15.2
# via -r requirements.in
shellingham==1.4.0
# via click-completion
signalslot==0.1.2
# via aioax25
six==1.16.0
# via
# -r requirements.in
# click-completion
# eventlet
# imapclient
# pyopenssl
# signalslot
soupsieve==2.3.1
# via beautifulsoup4
tabulate==0.8.9
# via -r requirements.in
thesmuggler==1.0.1
# via -r requirements.in
update-checker==0.18.0
# via -r requirements.in
urllib3==1.26.6
# via requests
weakrefmethod==1.0.3
# via signalslot
werkzeug==2.0.0
# via flask

View File

@ -15,7 +15,10 @@ F = t.TypeVar("F", bound=t.Callable[..., t.Any])
class TestDevTestPluginCommand(unittest.TestCase): class TestDevTestPluginCommand(unittest.TestCase):
def _build_config(self, login=None, password=None): def _build_config(self, login=None, password=None):
config = {"aprs": {}} config = {
"aprs": {},
"aprsd": {"trace": False},
}
if login: if login:
config["aprs"]["login"] = login config["aprs"]["login"] = login
@ -25,7 +28,7 @@ class TestDevTestPluginCommand(unittest.TestCase):
return aprsd_config.Config(config) return aprsd_config.Config(config)
@mock.patch("aprsd.config.parse_config") @mock.patch("aprsd.config.parse_config")
@mock.patch("aprsd.log.setup_logging") @mock.patch("aprsd.logging.log.setup_logging")
def test_no_login(self, mock_logging, mock_parse_config): def test_no_login(self, mock_logging, mock_parse_config):
"""Make sure we get an error if there is no login and config.""" """Make sure we get an error if there is no login and config."""
@ -43,7 +46,7 @@ class TestDevTestPluginCommand(unittest.TestCase):
assert "Must set --aprs_login or APRS_LOGIN" in result.output assert "Must set --aprs_login or APRS_LOGIN" in result.output
@mock.patch("aprsd.config.parse_config") @mock.patch("aprsd.config.parse_config")
@mock.patch("aprsd.log.setup_logging") @mock.patch("aprsd.logging.log.setup_logging")
def test_no_plugin_arg(self, mock_logging, mock_parse_config): def test_no_plugin_arg(self, mock_logging, mock_parse_config):
"""Make sure we get an error if there is no login and config.""" """Make sure we get an error if there is no login and config."""

View File

@ -15,7 +15,10 @@ F = t.TypeVar("F", bound=t.Callable[..., t.Any])
class TestSendMessageCommand(unittest.TestCase): class TestSendMessageCommand(unittest.TestCase):
def _build_config(self, login=None, password=None): def _build_config(self, login=None, password=None):
config = {"aprs": {}} config = {
"aprs": {},
"aprsd": {"trace": False},
}
if login: if login:
config["aprs"]["login"] = login config["aprs"]["login"] = login
@ -25,7 +28,7 @@ class TestSendMessageCommand(unittest.TestCase):
return aprsd_config.Config(config) return aprsd_config.Config(config)
@mock.patch("aprsd.config.parse_config") @mock.patch("aprsd.config.parse_config")
@mock.patch("aprsd.log.setup_logging") @mock.patch("aprsd.logging.log.setup_logging")
def test_no_login(self, mock_logging, mock_parse_config): def test_no_login(self, mock_logging, mock_parse_config):
"""Make sure we get an error if there is no login and config.""" """Make sure we get an error if there is no login and config."""
@ -43,7 +46,7 @@ class TestSendMessageCommand(unittest.TestCase):
assert "Must set --aprs_login or APRS_LOGIN" in result.output assert "Must set --aprs_login or APRS_LOGIN" in result.output
@mock.patch("aprsd.config.parse_config") @mock.patch("aprsd.config.parse_config")
@mock.patch("aprsd.log.setup_logging") @mock.patch("aprsd.logging.log.setup_logging")
def test_no_password(self, mock_logging, mock_parse_config): def test_no_password(self, mock_logging, mock_parse_config):
"""Make sure we get an error if there is no password and config.""" """Make sure we get an error if there is no password and config."""
@ -58,7 +61,7 @@ class TestSendMessageCommand(unittest.TestCase):
assert "Must set --aprs-password or APRS_PASSWORD" in result.output assert "Must set --aprs-password or APRS_PASSWORD" in result.output
@mock.patch("aprsd.config.parse_config") @mock.patch("aprsd.config.parse_config")
@mock.patch("aprsd.log.setup_logging") @mock.patch("aprsd.logging.log.setup_logging")
def test_no_tocallsign(self, mock_logging, mock_parse_config): def test_no_tocallsign(self, mock_logging, mock_parse_config):
"""Make sure we get an error if there is no tocallsign.""" """Make sure we get an error if there is no tocallsign."""
@ -76,7 +79,7 @@ class TestSendMessageCommand(unittest.TestCase):
assert "Error: Missing argument 'TOCALLSIGN'" in result.output assert "Error: Missing argument 'TOCALLSIGN'" in result.output
@mock.patch("aprsd.config.parse_config") @mock.patch("aprsd.config.parse_config")
@mock.patch("aprsd.log.setup_logging") @mock.patch("aprsd.logging.log.setup_logging")
def test_no_command(self, mock_logging, mock_parse_config): def test_no_command(self, mock_logging, mock_parse_config):
"""Make sure we get an error if there is no command.""" """Make sure we get an error if there is no command."""

View File

@ -2,8 +2,8 @@ from unittest import mock
import pytz import pytz
from aprsd.fuzzyclock import fuzzy
from aprsd.plugins import time as time_plugin from aprsd.plugins import time as time_plugin
from aprsd.utils import fuzzy
from .. import fake, test_plugin from .. import fake, test_plugin

View File

@ -3,6 +3,9 @@ minversion = 2.9.0
skipdist = True skipdist = True
skip_missing_interpreters = true skip_missing_interpreters = true
envlist = pre-commit,pep8,py{36,37,38,39} envlist = pre-commit,pep8,py{36,37,38,39}
#requires = tox-pipenv
# pip==22.0.4
# pip-tools==5.4.0
# Activate isolated build environment. tox will use a virtual environment # Activate isolated build environment. tox will use a virtual environment
# to build a source distribution from the source tree. For build tools and # to build a source distribution from the source tree. For build tools and