2
.github/workflows/python.yml
vendored
@ -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 }}
|
||||||
|
9
Makefile
@ -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
|
||||||
|
@ -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):
|
||||||
|
@ -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"]
|
||||||
|
@ -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,6 +52,7 @@ class Client:
|
|||||||
|
|
||||||
def reset(self):
|
def reset(self):
|
||||||
"""Call this to force a rebuild/reconnect."""
|
"""Call this to force a rebuild/reconnect."""
|
||||||
|
if self._client:
|
||||||
del self._client
|
del self._client
|
||||||
|
|
||||||
@abc.abstractmethod
|
@abc.abstractmethod
|
||||||
@ -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)
|
||||||
|
@ -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)
|
||||||
|
@ -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
|
||||||
path=["WIDE1-1", "WIDE2-1"],
|
# )).encode('US-ASCII'),
|
||||||
oneshot=True,
|
# 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"],
|
||||||
|
info=payload,
|
||||||
|
)
|
||||||
|
self.kiss.write(frame)
|
||||||
|
@ -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")
|
||||||
|
@ -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)
|
||||||
|
@ -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
@ -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.")
|
@ -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:
|
||||||
|
@ -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()
|
||||||
|
@ -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")
|
@ -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,
|
||||||
|
@ -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,12 +39,12 @@ 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
|
||||||
@ -53,16 +56,16 @@ class PacketList:
|
|||||||
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
|
||||||
|
|
||||||
|
|
||||||
@ -70,16 +73,17 @@ 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.data = {}
|
|
||||||
cls._instance._init_store()
|
cls._instance._init_store()
|
||||||
|
cls._instance.data = {}
|
||||||
return cls._instance
|
return cls._instance
|
||||||
|
|
||||||
def __init__(self, config=None):
|
def __init__(self, config=None):
|
||||||
@ -110,8 +114,8 @@ 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()
|
||||||
@ -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.data = {}
|
|
||||||
cls._instance._init_store()
|
cls._instance._init_store()
|
||||||
|
cls._instance.data = {}
|
||||||
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:
|
||||||
|
@ -492,4 +492,5 @@ class PluginManager:
|
|||||||
self._pluggy_pm.register(obj)
|
self._pluggy_pm.register(obj)
|
||||||
|
|
||||||
def get_plugins(self):
|
def get_plugins(self):
|
||||||
|
if self._pluggy_pm:
|
||||||
return self._pluggy_pm.get_plugins()
|
return self._pluggy_pm.get_plugins()
|
||||||
|
@ -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")
|
||||||
|
@ -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")
|
||||||
|
@ -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")
|
||||||
|
@ -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")
|
||||||
|
@ -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")
|
||||||
|
@ -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")
|
||||||
|
@ -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."""
|
||||||
|
|
||||||
|
@ -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")
|
||||||
|
@ -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")
|
||||||
|
@ -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,7 +187,7 @@ 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):
|
def full_name_with_qualname(obj):
|
||||||
return "{}.{}".format(
|
return "{}.{}".format(
|
||||||
obj.__class__.__module__,
|
obj.__class__.__module__,
|
||||||
@ -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
@ -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
@ -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")
|
88
aprsd/threads/keep_alive.py
Normal 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
|
@ -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,
|
||||||
)
|
)
|
@ -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)
|
|
37
aprsd/utils/ring_buffer.py
Normal 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
0
aprsd/web/admin/__init__.py
Normal file
BIN
aprsd/web/admin/static/images/Untitled.png
Normal file
After Width: | Height: | Size: 37 KiB |
Before Width: | Height: | Size: 52 KiB After Width: | Height: | Size: 52 KiB |
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 48 KiB |
Before Width: | Height: | Size: 52 KiB After Width: | Height: | Size: 52 KiB |
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 48 KiB |
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 40 KiB |
94
aprsd/web/chat/static/css/index.css
Normal 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;
|
||||||
|
}
|
1
aprsd/web/chat/static/css/style.css.map
Normal file
41
aprsd/web/chat/static/css/tabs.css
Normal 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;
|
||||||
|
}
|
BIN
aprsd/web/chat/static/images/Untitled.png
Normal file
After Width: | Height: | Size: 37 KiB |
BIN
aprsd/web/chat/static/images/aprs-symbols-16-0.png
Normal file
After Width: | Height: | Size: 52 KiB |
BIN
aprsd/web/chat/static/images/aprs-symbols-16-1.png
Normal file
After Width: | Height: | Size: 48 KiB |
BIN
aprsd/web/chat/static/images/aprs-symbols-64-0.png
Normal file
After Width: | Height: | Size: 52 KiB |
BIN
aprsd/web/chat/static/images/aprs-symbols-64-1.png
Normal file
After Width: | Height: | Size: 48 KiB |
BIN
aprsd/web/chat/static/images/aprs-symbols-64-2.png
Normal file
After Width: | Height: | Size: 40 KiB |
44
aprsd/web/chat/static/js/main.js
Normal 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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
}
|
215
aprsd/web/chat/static/js/send-message.js
Normal 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">> </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);
|
||||||
|
}
|
28
aprsd/web/chat/static/js/tabs.js
Normal 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";
|
||||||
|
}
|
||||||
|
}
|
57
aprsd/web/chat/static/json-viewer/jquery.json-viewer.css
Normal 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;
|
||||||
|
}
|
158
aprsd/web/chat/static/json-viewer/jquery.json-viewer.js
Normal 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, '&')
|
||||||
|
.replace(/</g, '<')
|
||||||
|
.replace(/>/g, '>')
|
||||||
|
.replace(/'/g, ''')
|
||||||
|
.replace(/"/g, '"');
|
||||||
|
|
||||||
|
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(/"/g, '\\"');
|
||||||
|
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);
|
86
aprsd/web/chat/templates/index.html
Normal 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;">
|
||||||
|
|
||||||
|
</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>
|
@ -10,3 +10,5 @@ pip-tools
|
|||||||
pytest
|
pytest
|
||||||
pytest-cov
|
pytest-cov
|
||||||
gray
|
gray
|
||||||
|
pip==22.0.4
|
||||||
|
pip-tools==5.4.0
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
175
requirements.txt
@ -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
|
|
||||||
|
@ -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."""
|
||||||
|
|
||||||
|
@ -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."""
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
3
tox.ini
@ -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
|
||||||
|