mirror of
https://github.com/craigerl/aprsd.git
synced 2026-08-16 00:23:34 -04:00
Compare commits
31 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 560e152742 | |||
| 69b215d4d8 | |||
| 4164e89016 | |||
| 1b9a9935fc | |||
| 3faf41b203 | |||
| 7e6dffb34b | |||
| 605911cb84 | |||
| 9eff99dde7 | |||
| d6b3df93f1 | |||
| 4f088e0a4a | |||
| d643ca3892 | |||
| dfaf3aa3d1 | |||
| 62ce84b315 | |||
| 8ada789d4d | |||
| 558710d348 | |||
| 1ea6c05dec | |||
| 0f6df5fc05 | |||
| 1635feb820 | |||
| c58031d772 | |||
| 266ae7f217 | |||
| c537b54df6 | |||
| 84ce60bc50 | |||
| c941379a5c | |||
| 23cbf32814 | |||
| 6d3258e833 | |||
| d243e577f0 | |||
| ca438c9c60 | |||
| f4dee4b202 | |||
| 54c9a6b55a | |||
| b53e2ba7fe | |||
| a7d79a6e1b |
@@ -1,9 +1,40 @@
|
||||
CHANGES
|
||||
=======
|
||||
|
||||
v2.3.0
|
||||
------
|
||||
|
||||
* Enable plugins to return message object
|
||||
* Added enabled flag for every plugin object
|
||||
* Ensure plugin threads are valid
|
||||
* Updated Dockerfile to use v2.3.0
|
||||
* Removed fixed size on logging queue
|
||||
* Added Logfile tab in Admin ui
|
||||
* Updated Makefile clean target
|
||||
* Added self creating Makefile help target
|
||||
* Update dev.py
|
||||
* Allow passing in aprsis\_client
|
||||
* Fixed a problem with the AVWX plugin not working
|
||||
* Remove some noisy trace in email plugin
|
||||
* Fixed issue at startup with notify plugin
|
||||
* Fixed email validation
|
||||
* Removed values from forms
|
||||
* Added send-message to the main admin UI
|
||||
* Updated requirements
|
||||
* Cleaned up some pep8 failures
|
||||
* Upgraded the send-message POC to use websockets
|
||||
* New Admin ui send message page working
|
||||
* Send Message via admin Web interface
|
||||
* Updated Admin UI to show KISS connections
|
||||
* Got TX/RX working with aioax25+direwolf over TCP
|
||||
* Rebased from master
|
||||
* Added the ability to use direwolf KISS socket
|
||||
* Update Dockerfile to use 2.2.1
|
||||
|
||||
v2.2.1
|
||||
------
|
||||
|
||||
* Update Changelog for 2.2.1
|
||||
* Silence some log noise
|
||||
|
||||
v2.2.0
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
REQUIREMENTS_TXT ?= requirements.txt dev-requirements.txt
|
||||
.DEFAULT_GOAL := help
|
||||
|
||||
.PHONY: dev docs server test
|
||||
include Makefile.venv
|
||||
Makefile.venv:
|
||||
curl \
|
||||
@@ -7,52 +9,65 @@ Makefile.venv:
|
||||
-L "https://github.com/sio/Makefile.venv/raw/v2020.08.14/Makefile.venv"
|
||||
echo "5afbcf51a82f629cd65ff23185acde90ebe4dec889ef80bbdc12562fbd0b2611 *Makefile.fetched" \
|
||||
| sha256sum --check - \
|
||||
&& mv Makefile.fetched Makefile.venv
|
||||
&& mv Makefile.fetched Makefile.venv
|
||||
|
||||
all: pip dev
|
||||
help: # Help for the Makefile
|
||||
@egrep -h '\s##\s' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-20s\033[0m %s\n", $$1, $$2}'
|
||||
|
||||
.PHONY: dev
|
||||
dev: venv
|
||||
$(VENV)/pre-commit install
|
||||
dev: venv ## Create the virtualenv with all the requirements installed
|
||||
|
||||
.PHONY: docs
|
||||
docs: build
|
||||
cp README.rst docs/readme.rst
|
||||
cp Changelog docs/changelog.rst
|
||||
tox -edocs
|
||||
|
||||
.PHONY: server
|
||||
server: venv
|
||||
$(VENV)/aprsd server --loglevel DEBUG
|
||||
clean: clean-build clean-pyc clean-test ## remove all build, test, coverage and Python artifacts
|
||||
|
||||
clean: clean-venv
|
||||
rm -rf dist/*
|
||||
clean-build: ## remove build artifacts
|
||||
rm -fr build/
|
||||
rm -fr dist/
|
||||
rm -fr .eggs/
|
||||
find . -name '*.egg-info' -exec rm -fr {} +
|
||||
find . -name '*.egg' -exec rm -f {} +
|
||||
|
||||
.PHONY: test
|
||||
test: dev
|
||||
clean-pyc: ## remove Python file artifacts
|
||||
find . -name '*.pyc' -exec rm -f {} +
|
||||
find . -name '*.pyo' -exec rm -f {} +
|
||||
find . -name '*~' -exec rm -f {} +
|
||||
find . -name '__pycache__' -exec rm -fr {} +
|
||||
|
||||
clean-test: ## remove test and coverage artifacts
|
||||
rm -fr .tox/
|
||||
rm -f .coverage
|
||||
rm -fr htmlcov/
|
||||
rm -fr .pytest_cache
|
||||
|
||||
test: dev ## Run all the tox tests
|
||||
tox -p all
|
||||
|
||||
build: test
|
||||
build: test ## Make the build artifact prior to doing an upload
|
||||
$(VENV)/python3 setup.py sdist bdist_wheel
|
||||
$(VENV)/twine check dist/*
|
||||
|
||||
upload: build
|
||||
upload: build ## Upload a new version of the plugin
|
||||
$(VENV)/twine upload dist/*
|
||||
|
||||
docker: test
|
||||
docker build -t hemna6969/aprsd:latest -f docker/Dockerfile docker
|
||||
|
||||
docker-dev: test
|
||||
docker build -t hemna6969/aprsd:master -f docker/Dockerfile-dev docker
|
||||
|
||||
update-requirements: dev
|
||||
$(VENV)/pip-compile requirements.in
|
||||
$(VENV)/pip-compile dev-requirements.in
|
||||
|
||||
|
||||
check: dev # Code format check with isort and black
|
||||
check: dev ## Code format check with tox and pep8
|
||||
tox -efmt-check
|
||||
tox -epep8
|
||||
|
||||
fix: dev # fixes code formatting with isort and black
|
||||
fix: dev ## fixes code formatting with gray
|
||||
tox -efmt
|
||||
|
||||
server: venv ## Create the virtual environment and run aprsd server --loglevel DEBUG
|
||||
$(VENV)/aprsd server --loglevel DEBUG
|
||||
|
||||
docker: test ## Make a docker container tagged with hemna6969/aprsd:latest
|
||||
docker build -t hemna6969/aprsd:latest -f docker/Dockerfile docker
|
||||
|
||||
docker-dev: test ## Make a development docker container tagged with hemna6969/aprsd:master
|
||||
docker build -t hemna6969/aprsd:master -f docker/Dockerfile-dev docker
|
||||
|
||||
update-requirements: dev ## Update the requirements.txt and dev-requirements.txt files
|
||||
$(VENV)/pip-compile requirements.in
|
||||
$(VENV)/pip-compile dev-requirements.in
|
||||
|
||||
+21
-4
@@ -38,6 +38,11 @@ class Client:
|
||||
if config:
|
||||
self.config = config
|
||||
|
||||
def new(self):
|
||||
obj = super().__new__(Client)
|
||||
obj.config = self.config
|
||||
return obj
|
||||
|
||||
@property
|
||||
def client(self):
|
||||
if not self.aprs_client:
|
||||
@@ -90,6 +95,11 @@ class Aprsdis(aprslib.IS):
|
||||
self.thread_stop = True
|
||||
LOG.info("Shutdown Aprsdis client.")
|
||||
|
||||
def send(self, msg):
|
||||
"""Send an APRS Message object."""
|
||||
line = str(msg)
|
||||
self.sendall(line)
|
||||
|
||||
def _socket_readlines(self, blocking=False):
|
||||
"""
|
||||
Generator for complete lines, received from the server
|
||||
@@ -113,15 +123,22 @@ class Aprsdis(aprslib.IS):
|
||||
self.select_timeout,
|
||||
)
|
||||
if not readable:
|
||||
continue
|
||||
if not blocking:
|
||||
break
|
||||
else:
|
||||
continue
|
||||
|
||||
try:
|
||||
short_buf = self.sock.recv(4096)
|
||||
|
||||
# sock.recv returns empty if the connection drops
|
||||
if not short_buf:
|
||||
self.logger.error("socket.recv(): returned empty")
|
||||
raise aprslib.ConnectionDrop("connection dropped")
|
||||
if not blocking:
|
||||
# We could just not be blocking, so empty is expected
|
||||
continue
|
||||
else:
|
||||
self.logger.error("socket.recv(): returned empty")
|
||||
raise aprslib.ConnectionDrop("connection dropped")
|
||||
except OSError as e:
|
||||
# self.logger.error("socket error on recv(): %s" % str(e))
|
||||
if "Resource temporarily unavailable" in str(e):
|
||||
@@ -210,7 +227,7 @@ class Aprsdis(aprslib.IS):
|
||||
|
||||
line = b""
|
||||
|
||||
while True:
|
||||
while True and not self.thread_stop:
|
||||
try:
|
||||
for line in self._socket_readlines(blocking):
|
||||
if line[0:1] != b"#":
|
||||
|
||||
+7
-1
@@ -189,8 +189,14 @@ def test_plugin(
|
||||
|
||||
pm = plugin.PluginManager(config)
|
||||
obj = pm._create_class(plugin_path, plugin.APRSDPluginBase, config=config)
|
||||
login = config["aprs"]["login"]
|
||||
|
||||
packet = {"from": fromcall, "message_text": message, "msgNo": 1}
|
||||
packet = {
|
||||
"from": fromcall, "addresse": login,
|
||||
"message_text": message,
|
||||
"format": "message",
|
||||
"msgNo": 1,
|
||||
}
|
||||
|
||||
reply = obj.filter(packet)
|
||||
# Plugin might have threads, so lets stop them so we can exit.
|
||||
|
||||
+412
-2
@@ -4,14 +4,22 @@ import logging
|
||||
from logging import NullHandler
|
||||
from logging.handlers import RotatingFileHandler
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
|
||||
import aprslib
|
||||
from aprslib.exceptions import LoginError
|
||||
import flask
|
||||
from flask import request
|
||||
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 aprsd
|
||||
from aprsd import messaging, packets, plugin, stats, utils
|
||||
from aprsd import (
|
||||
client, kissclient, messaging, packets, plugin, stats, threads, utils,
|
||||
)
|
||||
|
||||
|
||||
LOG = logging.getLogger("APRSD")
|
||||
@@ -20,6 +28,72 @@ auth = HTTPBasicAuth()
|
||||
users = None
|
||||
|
||||
|
||||
class SentMessages:
|
||||
_instance = None
|
||||
lock = None
|
||||
|
||||
msgs = {}
|
||||
|
||||
def __new__(cls, *args, **kwargs):
|
||||
"""This magic turns this into a singleton."""
|
||||
if cls._instance is None:
|
||||
cls._instance = super().__new__(cls)
|
||||
# Put any initialization here.
|
||||
cls.lock = threading.Lock()
|
||||
return cls._instance
|
||||
|
||||
def add(self, msg):
|
||||
with self.lock:
|
||||
self.msgs[msg.id] = self._create(msg.id)
|
||||
self.msgs[msg.id]["from"] = msg.fromcall
|
||||
self.msgs[msg.id]["to"] = msg.tocall
|
||||
self.msgs[msg.id]["message"] = msg.message.rstrip("\n")
|
||||
self.msgs[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,
|
||||
}
|
||||
|
||||
def __len__(self):
|
||||
with self.lock:
|
||||
return len(self.msgs.keys())
|
||||
|
||||
def get(self, id):
|
||||
with self.lock:
|
||||
if id in self.msgs:
|
||||
return self.msgs[id]
|
||||
|
||||
def get_all(self):
|
||||
with self.lock:
|
||||
return self.msgs
|
||||
|
||||
def set_status(self, id, status):
|
||||
with self.lock:
|
||||
self.msgs[id]["last_update"] = str(datetime.datetime.now())
|
||||
self.msgs[id]["status"] = status
|
||||
|
||||
def ack(self, id):
|
||||
"""The message got an ack!"""
|
||||
with self.lock:
|
||||
self.msgs[id]["last_update"] = str(datetime.datetime.now())
|
||||
self.msgs[id]["ack"] = True
|
||||
|
||||
def reply(self, id, packet):
|
||||
"""We got a packet back from the sent message."""
|
||||
with self.lock:
|
||||
self.msgs[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
|
||||
@@ -31,6 +105,171 @@ def verify_password(username, password):
|
||||
return username
|
||||
|
||||
|
||||
class SendMessageThread(threads.APRSDThread):
|
||||
"""Thread for sending a message from web."""
|
||||
|
||||
aprsis_client = None
|
||||
request = None
|
||||
got_ack = False
|
||||
got_reply = False
|
||||
|
||||
def __init__(self, config, info, msg, namespace):
|
||||
self.config = config
|
||||
self.request = info
|
||||
self.msg = msg
|
||||
self.namespace = namespace
|
||||
self.start_time = datetime.datetime.now()
|
||||
msg = "({} -> {}) : {}".format(
|
||||
info["from"],
|
||||
info["to"],
|
||||
info["message"],
|
||||
)
|
||||
super().__init__(f"WEB_SEND_MSG-{msg}")
|
||||
|
||||
def setup_connection(self):
|
||||
user = self.request["from"]
|
||||
password = self.request["password"]
|
||||
host = self.config["aprs"].get("host", "rotate.aprs.net")
|
||||
port = self.config["aprs"].get("port", 14580)
|
||||
connected = False
|
||||
backoff = 1
|
||||
while not connected:
|
||||
try:
|
||||
LOG.info("Creating aprslib client")
|
||||
aprs_client = client.Aprsdis(
|
||||
user,
|
||||
passwd=password,
|
||||
host=host,
|
||||
port=port,
|
||||
)
|
||||
# Force the logging to be the same
|
||||
aprs_client.logger = LOG
|
||||
aprs_client.connect()
|
||||
connected = True
|
||||
backoff = 1
|
||||
except LoginError as e:
|
||||
LOG.error(f"Failed to login to APRS-IS Server '{e}'")
|
||||
connected = False
|
||||
raise e
|
||||
except Exception as e:
|
||||
LOG.error(f"Unable to connect to APRS-IS server. '{e}' ")
|
||||
time.sleep(backoff)
|
||||
backoff = backoff * 2
|
||||
continue
|
||||
LOG.debug(f"Logging in to APRS-IS with user '{user}'")
|
||||
return aprs_client
|
||||
|
||||
def run(self):
|
||||
LOG.debug("Starting")
|
||||
from_call = self.request["from"]
|
||||
to_call = self.request["to"]
|
||||
message = self.request["message"]
|
||||
LOG.info(
|
||||
"From: '{}' To: '{}' Send '{}'".format(
|
||||
from_call,
|
||||
to_call,
|
||||
message,
|
||||
),
|
||||
)
|
||||
|
||||
try:
|
||||
self.aprs_client = self.setup_connection()
|
||||
except LoginError as e:
|
||||
f"Failed to setup Connection {e}"
|
||||
|
||||
self.msg.send_direct(aprsis_client=self.aprs_client)
|
||||
SentMessages().set_status(self.msg.id, "Sent")
|
||||
|
||||
while not self.thread_stop:
|
||||
can_loop = self.loop()
|
||||
if not can_loop:
|
||||
self.stop()
|
||||
threads.APRSDThreadList().remove(self)
|
||||
LOG.debug("Exiting")
|
||||
|
||||
def rx_packet(self, packet):
|
||||
global socketio
|
||||
# LOG.debug("Got packet back {}".format(packet))
|
||||
resp = packet.get("response", None)
|
||||
if resp == "ack":
|
||||
ack_num = packet.get("msgNo")
|
||||
LOG.info(f"We got ack for our sent message {ack_num}")
|
||||
messaging.log_packet(packet)
|
||||
SentMessages().ack(self.msg.id)
|
||||
socketio.emit(
|
||||
"ack", SentMessages().get(self.msg.id),
|
||||
namespace="/sendmsg",
|
||||
)
|
||||
stats.APRSDStats().ack_rx_inc()
|
||||
self.got_ack = True
|
||||
if self.request["wait_reply"] == "0" or self.got_reply:
|
||||
# We aren't waiting for a reply, so we can bail
|
||||
self.stop()
|
||||
self.thread_stop = self.aprs_client.thread_stop = True
|
||||
else:
|
||||
packets.PacketList().add(packet)
|
||||
stats.APRSDStats().msgs_rx_inc()
|
||||
message = packet.get("message_text", None)
|
||||
fromcall = packet["from"]
|
||||
msg_number = packet.get("msgNo", "0")
|
||||
messaging.log_message(
|
||||
"Received Message",
|
||||
packet["raw"],
|
||||
message,
|
||||
fromcall=fromcall,
|
||||
ack=msg_number,
|
||||
)
|
||||
SentMessages().reply(self.msg.id, packet)
|
||||
SentMessages().set_status(self.msg.id, "Got Reply")
|
||||
socketio.emit(
|
||||
"reply", SentMessages().get(self.msg.id),
|
||||
namespace="/sendmsg",
|
||||
)
|
||||
|
||||
# Send the ack back?
|
||||
ack = messaging.AckMessage(
|
||||
self.request["from"],
|
||||
fromcall,
|
||||
msg_id=msg_number,
|
||||
)
|
||||
ack.send_direct()
|
||||
SentMessages().set_status(self.msg.id, "Ack Sent")
|
||||
|
||||
# Now we can exit, since we are done.
|
||||
self.got_reply = True
|
||||
if self.got_ack:
|
||||
self.stop()
|
||||
self.thread_stop = self.aprs_client.thread_stop = True
|
||||
|
||||
def loop(self):
|
||||
# we have a general time limit expecting results of
|
||||
# around 120 seconds before we exit
|
||||
now = datetime.datetime.now()
|
||||
start_delta = str(now - self.start_time)
|
||||
delta = utils.parse_delta_str(start_delta)
|
||||
d = datetime.timedelta(**delta)
|
||||
max_timeout = {"hours": 0.0, "minutes": 1, "seconds": 0}
|
||||
max_delta = datetime.timedelta(**max_timeout)
|
||||
if d > max_delta:
|
||||
LOG.error("XXXXXX Haven't completed everything in 60 seconds. BAIL!")
|
||||
return False
|
||||
|
||||
if self.got_ack and self.got_reply:
|
||||
LOG.warning("We got everything already. BAIL")
|
||||
return False
|
||||
|
||||
try:
|
||||
# This will register a packet consumer with aprslib
|
||||
# When new packets come in the consumer will process
|
||||
# the packet
|
||||
self.aprs_client.consumer(self.rx_packet, raw=False, blocking=False)
|
||||
except aprslib.exceptions.ConnectionDrop:
|
||||
LOG.error("Connection dropped.")
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class APRSDFlask(flask_classful.FlaskView):
|
||||
config = None
|
||||
|
||||
@@ -65,9 +304,38 @@ class APRSDFlask(flask_classful.FlaskView):
|
||||
plugins = pm.get_plugins()
|
||||
plugin_count = len(plugins)
|
||||
|
||||
if self.config["aprs"].get("enabled", True):
|
||||
transport = "aprs-is"
|
||||
aprs_connection = (
|
||||
"APRS-IS Server: <a href='http://status.aprs2.net' >"
|
||||
"{}</a>".format(stats["stats"]["aprs-is"]["server"])
|
||||
)
|
||||
else:
|
||||
# We might be connected to a KISS socket?
|
||||
if kissclient.KISSClient.kiss_enabled(self.config):
|
||||
transport = kissclient.KISSClient.transport(self.config)
|
||||
if transport == kissclient.TRANSPORT_TCPKISS:
|
||||
aprs_connection = (
|
||||
"TCPKISS://{}:{}".format(
|
||||
self.config["kiss"]["tcp"]["host"],
|
||||
self.config["kiss"]["tcp"]["port"],
|
||||
)
|
||||
)
|
||||
elif transport == kissclient.TRANSPORT_SERIALKISS:
|
||||
aprs_connection = (
|
||||
"SerialKISS://{}@{} baud".format(
|
||||
self.config["kiss"]["serial"]["device"],
|
||||
self.config["kiss"]["serial"]["baudrate"],
|
||||
)
|
||||
)
|
||||
|
||||
stats["transport"] = transport
|
||||
stats["aprs_connection"] = aprs_connection
|
||||
|
||||
return flask.render_template(
|
||||
"index.html",
|
||||
initial_stats=stats,
|
||||
aprs_connection=aprs_connection,
|
||||
callsign=self.config["aprs"]["login"],
|
||||
version=aprsd.__version__,
|
||||
config_json=json.dumps(self.config),
|
||||
@@ -86,6 +354,23 @@ class APRSDFlask(flask_classful.FlaskView):
|
||||
|
||||
return flask.render_template("messages.html", messages=json.dumps(msgs))
|
||||
|
||||
@auth.login_required
|
||||
def send_message_status(self):
|
||||
LOG.debug(request)
|
||||
msgs = SentMessages()
|
||||
info = msgs.get_all()
|
||||
return json.dumps(info)
|
||||
|
||||
@auth.login_required
|
||||
def send_message(self):
|
||||
LOG.debug(request)
|
||||
if request.method == "GET":
|
||||
return flask.render_template(
|
||||
"send-message.html",
|
||||
callsign=self.config["aprs"]["login"],
|
||||
version=aprsd.__version__,
|
||||
)
|
||||
|
||||
@auth.login_required
|
||||
def packets(self):
|
||||
packet_list = packets.PacketList().get()
|
||||
@@ -148,6 +433,117 @@ class APRSDFlask(flask_classful.FlaskView):
|
||||
return json.dumps(self._stats())
|
||||
|
||||
|
||||
class SendMessageNamespace(Namespace):
|
||||
_config = None
|
||||
got_ack = False
|
||||
reply_sent = False
|
||||
msg = None
|
||||
request = None
|
||||
|
||||
def __init__(self, namespace=None, config=None):
|
||||
self._config = config
|
||||
super().__init__(namespace)
|
||||
|
||||
def on_connect(self):
|
||||
global socketio
|
||||
LOG.debug("Web socket connected")
|
||||
socketio.emit(
|
||||
"connected", {"data": "/sendmsg Connected"},
|
||||
namespace="/sendmsg",
|
||||
)
|
||||
|
||||
def on_disconnect(self):
|
||||
LOG.debug("WS Disconnected")
|
||||
|
||||
def on_send(self, data):
|
||||
global socketio
|
||||
LOG.debug(f"WS: on_send {data}")
|
||||
self.request = data
|
||||
msg = messaging.TextMessage(
|
||||
data["from"], data["to"],
|
||||
data["message"],
|
||||
)
|
||||
self.msg = msg
|
||||
msgs = SentMessages()
|
||||
msgs.add(msg)
|
||||
msgs.set_status(msg.id, "Sending")
|
||||
socketio.emit(
|
||||
"sent", SentMessages().get(self.msg.id),
|
||||
namespace="/sendmsg",
|
||||
)
|
||||
|
||||
socketio.start_background_task(self._start, self._config, data, msg, self)
|
||||
LOG.warning("WS: on_send: exit")
|
||||
|
||||
def _start(self, config, data, msg, namespace):
|
||||
msg_thread = SendMessageThread(self._config, data, msg, self)
|
||||
msg_thread.start()
|
||||
|
||||
def handle_message(self, data):
|
||||
LOG.debug(f"WS Data {data}")
|
||||
|
||||
def handle_json(self, data):
|
||||
LOG.debug(f"WS json {data}")
|
||||
|
||||
|
||||
class LogMonitorThread(threads.APRSDThread):
|
||||
|
||||
def __init__(self):
|
||||
super().__init__("LogMonitorThread")
|
||||
|
||||
def loop(self):
|
||||
global socketio
|
||||
try:
|
||||
record = threads.logging_queue.get(block=True, timeout=5)
|
||||
json_record = self.json_record(record)
|
||||
socketio.emit(
|
||||
"log_entry", json_record,
|
||||
namespace="/logs",
|
||||
)
|
||||
except Exception:
|
||||
# Just ignore thi
|
||||
pass
|
||||
|
||||
return True
|
||||
|
||||
def json_record(self, record):
|
||||
entry = {}
|
||||
entry["filename"] = record.filename
|
||||
entry["funcName"] = record.funcName
|
||||
entry["levelname"] = record.levelname
|
||||
entry["lineno"] = record.lineno
|
||||
entry["module"] = record.module
|
||||
entry["name"] = record.name
|
||||
entry["pathname"] = record.pathname
|
||||
entry["process"] = record.process
|
||||
entry["processName"] = record.processName
|
||||
if hasattr(record, "stack_info"):
|
||||
entry["stack_info"] = record.stack_info
|
||||
else:
|
||||
entry["stack_info"] = None
|
||||
entry["thread"] = record.thread
|
||||
entry["threadName"] = record.threadName
|
||||
entry["message"] = record.getMessage()
|
||||
return entry
|
||||
|
||||
|
||||
class LoggingNamespace(Namespace):
|
||||
|
||||
def on_connect(self):
|
||||
global socketio
|
||||
LOG.debug("Web socket connected")
|
||||
socketio.emit(
|
||||
"connected", {"data": "/logs Connected"},
|
||||
namespace="/logs",
|
||||
)
|
||||
self.log_thread = LogMonitorThread()
|
||||
self.log_thread.start()
|
||||
|
||||
def on_disconnect(self):
|
||||
LOG.debug("WS Disconnected")
|
||||
self.log_thread.stop()
|
||||
|
||||
|
||||
def setup_logging(config, flask_app, loglevel, quiet):
|
||||
flask_log = logging.getLogger("werkzeug")
|
||||
|
||||
@@ -182,6 +578,8 @@ def setup_logging(config, flask_app, loglevel, quiet):
|
||||
|
||||
|
||||
def init_flask(config, loglevel, quiet):
|
||||
global socketio
|
||||
|
||||
flask_app = flask.Flask(
|
||||
"aprsd",
|
||||
static_url_path="/static",
|
||||
@@ -195,6 +593,18 @@ def init_flask(config, loglevel, quiet):
|
||||
flask_app.route("/stats", methods=["GET"])(server.stats)
|
||||
flask_app.route("/messages", methods=["GET"])(server.messages)
|
||||
flask_app.route("/packets", methods=["GET"])(server.packets)
|
||||
flask_app.route("/send-message", methods=["GET"])(server.send_message)
|
||||
flask_app.route("/send-message-status", methods=["GET"])(server.send_message_status)
|
||||
flask_app.route("/save", methods=["GET"])(server.save)
|
||||
flask_app.route("/plugins", methods=["GET"])(server.plugins)
|
||||
return flask_app
|
||||
|
||||
socketio = SocketIO(
|
||||
flask_app, logger=False, engineio_logger=False,
|
||||
async_mode="threading",
|
||||
)
|
||||
# import eventlet
|
||||
# eventlet.monkey_patch()
|
||||
|
||||
socketio.on_namespace(SendMessageNamespace("/sendmsg", config=config))
|
||||
socketio.on_namespace(LoggingNamespace("/logs"))
|
||||
return socketio, flask_app
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
from aioax25 import interface
|
||||
from aioax25 import kiss as kiss
|
||||
from aioax25.aprs import APRSInterface
|
||||
|
||||
from aprsd import trace
|
||||
|
||||
|
||||
TRANSPORT_TCPKISS = "tcpkiss"
|
||||
TRANSPORT_SERIALKISS = "serialkiss"
|
||||
LOG = logging.getLogger("APRSD")
|
||||
|
||||
|
||||
class KISSClient:
|
||||
|
||||
_instance = None
|
||||
config = None
|
||||
ax25client = None
|
||||
loop = None
|
||||
|
||||
def __new__(cls, *args, **kwargs):
|
||||
"""Singleton for this class."""
|
||||
if cls._instance is None:
|
||||
cls._instance = super().__new__(cls)
|
||||
# initialize shit here
|
||||
return cls._instance
|
||||
|
||||
def __init__(self, config=None):
|
||||
if config:
|
||||
self.config = config
|
||||
|
||||
@staticmethod
|
||||
def kiss_enabled(config):
|
||||
"""Return if tcp or serial KISS is enabled."""
|
||||
if "kiss" not in config:
|
||||
return False
|
||||
|
||||
if "serial" in config["kiss"]:
|
||||
if config["kiss"]["serial"].get("enabled", False):
|
||||
return True
|
||||
|
||||
if "tcp" in config["kiss"]:
|
||||
if config["kiss"]["tcp"].get("enabled", False):
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def transport(config):
|
||||
if "serial" in config["kiss"]:
|
||||
if config["kiss"]["serial"].get("enabled", False):
|
||||
return TRANSPORT_SERIALKISS
|
||||
|
||||
if "tcp" in config["kiss"]:
|
||||
if config["kiss"]["tcp"].get("enabled", False):
|
||||
return TRANSPORT_TCPKISS
|
||||
|
||||
@property
|
||||
def client(self):
|
||||
if not self.ax25client:
|
||||
self.ax25client = self.setup_connection()
|
||||
return self.ax25client
|
||||
|
||||
def reset(self):
|
||||
"""Call this to fore a rebuild/reconnect."""
|
||||
self.ax25client.stop()
|
||||
del self.ax25client
|
||||
|
||||
@trace.trace
|
||||
def setup_connection(self):
|
||||
ax25client = Aioax25Client(self.config)
|
||||
LOG.debug("Complete")
|
||||
return ax25client
|
||||
|
||||
|
||||
class Aioax25Client:
|
||||
def __init__(self, config):
|
||||
self.config = config
|
||||
self.setup()
|
||||
|
||||
def setup(self):
|
||||
# we can be TCP kiss or Serial kiss
|
||||
|
||||
self.loop = asyncio.get_event_loop()
|
||||
if "serial" in self.config["kiss"] and self.config["kiss"]["serial"].get(
|
||||
"enabled",
|
||||
False,
|
||||
):
|
||||
LOG.debug(
|
||||
"Setting up Serial KISS connection to {}".format(
|
||||
self.config["kiss"]["serial"]["device"],
|
||||
),
|
||||
)
|
||||
self.kissdev = kiss.SerialKISSDevice(
|
||||
device=self.config["kiss"]["serial"]["device"],
|
||||
baudrate=self.config["kiss"]["serial"].get("baudrate", 9600),
|
||||
loop=self.loop,
|
||||
)
|
||||
elif "tcp" in self.config["kiss"] and self.config["kiss"]["tcp"].get(
|
||||
"enabled",
|
||||
False,
|
||||
):
|
||||
LOG.debug(
|
||||
"Setting up KISSTCP Connection to {}:{}".format(
|
||||
self.config["kiss"]["tcp"]["host"],
|
||||
self.config["kiss"]["tcp"]["port"],
|
||||
),
|
||||
)
|
||||
self.kissdev = kiss.TCPKISSDevice(
|
||||
self.config["kiss"]["tcp"]["host"],
|
||||
self.config["kiss"]["tcp"]["port"],
|
||||
loop=self.loop,
|
||||
log=LOG,
|
||||
)
|
||||
|
||||
self.kissdev.open()
|
||||
self.kissport0 = self.kissdev[0]
|
||||
|
||||
LOG.debug("Creating AX25Interface")
|
||||
self.ax25int = interface.AX25Interface(kissport=self.kissport0, loop=self.loop)
|
||||
|
||||
LOG.debug("Creating APRSInterface")
|
||||
self.aprsint = APRSInterface(
|
||||
ax25int=self.ax25int,
|
||||
mycall=self.config["kiss"]["callsign"],
|
||||
log=LOG,
|
||||
)
|
||||
|
||||
def stop(self):
|
||||
LOG.debug(self.kissdev)
|
||||
self.kissdev._close()
|
||||
self.loop.stop()
|
||||
|
||||
def consumer(self, callback, callsign=None):
|
||||
if not callsign:
|
||||
callsign = self.config["ham"]["callsign"]
|
||||
self.aprsint.bind(callback=callback, callsign="WB4BOR", ssid=12, regex=False)
|
||||
|
||||
def send(self, msg):
|
||||
"""Send an APRS Message object."""
|
||||
payload = f"{msg._filter_for_send()}"
|
||||
self.aprsint.send_message(
|
||||
addressee=msg.tocall,
|
||||
message=payload,
|
||||
path=["WIDE1-1", "WIDE2-1"],
|
||||
oneshot=True,
|
||||
)
|
||||
|
||||
|
||||
def get_client():
|
||||
cl = KISSClient()
|
||||
return cl.client
|
||||
+47
-23
@@ -37,7 +37,8 @@ import click_completion
|
||||
# local imports here
|
||||
import aprsd
|
||||
from aprsd import (
|
||||
client, flask, messaging, packets, plugin, stats, threads, trace, utils,
|
||||
client, flask, kissclient, messaging, packets, plugin, stats, threads,
|
||||
trace, utils,
|
||||
)
|
||||
|
||||
|
||||
@@ -194,6 +195,20 @@ def setup_logging(config, loglevel, quiet):
|
||||
imap_logger.setLevel(log_level)
|
||||
imap_logger.addHandler(fh)
|
||||
|
||||
if (
|
||||
utils.check_config_option(
|
||||
config, ["aprsd", "web", "enabled"],
|
||||
default_fail=False,
|
||||
)
|
||||
):
|
||||
qh = logging.handlers.QueueHandler(threads.logging_queue)
|
||||
q_log_formatter = logging.Formatter(
|
||||
fmt=utils.QUEUE_LOG_FORMAT,
|
||||
datefmt=utils.QUEUE_DATE_FORMAT,
|
||||
)
|
||||
qh.setFormatter(q_log_formatter)
|
||||
LOG.addHandler(qh)
|
||||
|
||||
if not quiet:
|
||||
sh = logging.StreamHandler(sys.stdout)
|
||||
sh.setFormatter(log_formatter)
|
||||
@@ -361,6 +376,9 @@ def send_message(
|
||||
except LoginError:
|
||||
sys.exit(-1)
|
||||
|
||||
packets.PacketList(config=config)
|
||||
packets.WatchList(config=config)
|
||||
|
||||
# Send a message
|
||||
# then we setup a consumer to rx messages
|
||||
# We should get an ack back as well as a new message
|
||||
@@ -458,11 +476,23 @@ def server(
|
||||
trace.setup_tracing(["method", "api"])
|
||||
stats.APRSDStats(config)
|
||||
|
||||
try:
|
||||
cl = client.Client(config)
|
||||
cl.client
|
||||
except LoginError:
|
||||
sys.exit(-1)
|
||||
if config["aprs"].get("enabled", True):
|
||||
try:
|
||||
cl = client.Client(config)
|
||||
cl.client
|
||||
except LoginError:
|
||||
sys.exit(-1)
|
||||
|
||||
rx_thread = threads.APRSDRXThread(
|
||||
msg_queues=threads.msg_queues,
|
||||
config=config,
|
||||
)
|
||||
rx_thread.start()
|
||||
else:
|
||||
LOG.info(
|
||||
"APRS network connection Not Enabled in config. This is"
|
||||
" for setups without internet connectivity.",
|
||||
)
|
||||
|
||||
# Create the initial PM singleton and Register plugins
|
||||
plugin_manager = plugin.PluginManager(config)
|
||||
@@ -478,34 +508,28 @@ def server(
|
||||
messaging.MsgTrack().load()
|
||||
|
||||
packets.PacketList(config=config)
|
||||
packets.WatchList(config=config)
|
||||
|
||||
rx_thread = threads.APRSDRXThread(
|
||||
msg_queues=threads.msg_queues,
|
||||
config=config,
|
||||
)
|
||||
if kissclient.KISSClient.kiss_enabled(config):
|
||||
kcl = kissclient.KISSClient(config=config)
|
||||
# This initializes the client object.
|
||||
kcl.client
|
||||
|
||||
rx_thread.start()
|
||||
|
||||
if "watch_list" in config["aprsd"] and config["aprsd"]["watch_list"].get(
|
||||
"enabled",
|
||||
True,
|
||||
):
|
||||
packets.WatchList(config=config)
|
||||
kissrx_thread = threads.KISSRXThread(msg_queues=threads.msg_queues, config=config)
|
||||
kissrx_thread.start()
|
||||
|
||||
messaging.MsgTrack().restart()
|
||||
|
||||
keepalive = threads.KeepAliveThread(config=config)
|
||||
keepalive.start()
|
||||
|
||||
try:
|
||||
web_enabled = utils.check_config_option(config, ["aprsd", "web", "enabled"])
|
||||
except Exception:
|
||||
web_enabled = False
|
||||
web_enabled = utils.check_config_option(config, ["aprsd", "web", "enabled"], default_fail=False)
|
||||
|
||||
if web_enabled:
|
||||
flask_enabled = True
|
||||
app = flask.init_flask(config, loglevel, quiet)
|
||||
app.run(
|
||||
(socketio, app) = flask.init_flask(config, loglevel, quiet)
|
||||
socketio.run(
|
||||
app,
|
||||
host=config["aprsd"]["web"]["host"],
|
||||
port=config["aprsd"]["web"]["port"],
|
||||
)
|
||||
|
||||
+58
-21
@@ -9,7 +9,7 @@ import re
|
||||
import threading
|
||||
import time
|
||||
|
||||
from aprsd import client, packets, stats, threads, trace, utils
|
||||
from aprsd import client, kissclient, packets, stats, threads, trace, utils
|
||||
|
||||
|
||||
LOG = logging.getLogger("APRSD")
|
||||
@@ -18,6 +18,10 @@ LOG = logging.getLogger("APRSD")
|
||||
# and it's ok, but don't send a usage string back
|
||||
NULL_MESSAGE = -1
|
||||
|
||||
MESSAGE_TRANSPORT_TCPKISS = "tcpkiss"
|
||||
MESSAGE_TRANSPORT_SERIALKISS = "serialkiss"
|
||||
MESSAGE_TRANSPORT_APRSIS = "aprsis"
|
||||
|
||||
|
||||
class MsgTrack:
|
||||
"""Class to keep track of outstanding text messages.
|
||||
@@ -228,7 +232,15 @@ class Message(metaclass=abc.ABCMeta):
|
||||
last_send_time = 0
|
||||
last_send_attempt = 0
|
||||
|
||||
def __init__(self, fromcall, tocall, msg_id=None):
|
||||
transport = None
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
fromcall,
|
||||
tocall,
|
||||
msg_id=None,
|
||||
transport=MESSAGE_TRANSPORT_APRSIS,
|
||||
):
|
||||
self.fromcall = fromcall
|
||||
self.tocall = tocall
|
||||
if not msg_id:
|
||||
@@ -236,11 +248,18 @@ class Message(metaclass=abc.ABCMeta):
|
||||
c.increment()
|
||||
msg_id = c.value
|
||||
self.id = msg_id
|
||||
self.transport = transport
|
||||
|
||||
@abc.abstractmethod
|
||||
def send(self):
|
||||
"""Child class must declare."""
|
||||
|
||||
def get_transport(self):
|
||||
if self.transport == MESSAGE_TRANSPORT_APRSIS:
|
||||
return client.get_client()
|
||||
elif self.transport == MESSAGE_TRANSPORT_TCPKISS:
|
||||
return kissclient.get_client()
|
||||
|
||||
|
||||
class RawMessage(Message):
|
||||
"""Send a raw message.
|
||||
@@ -252,8 +271,8 @@ class RawMessage(Message):
|
||||
|
||||
message = None
|
||||
|
||||
def __init__(self, message):
|
||||
super().__init__(None, None, msg_id=None)
|
||||
def __init__(self, message, transport=MESSAGE_TRANSPORT_APRSIS):
|
||||
super().__init__(None, None, msg_id=None, transport=transport)
|
||||
self.message = message
|
||||
|
||||
def dict(self):
|
||||
@@ -280,9 +299,9 @@ class RawMessage(Message):
|
||||
thread = SendMessageThread(message=self)
|
||||
thread.start()
|
||||
|
||||
def send_direct(self):
|
||||
def send_direct(self, aprsis_client=None):
|
||||
"""Send a message without a separate thread."""
|
||||
cl = client.get_client()
|
||||
cl = self.get_transport()
|
||||
log_message(
|
||||
"Sending Message Direct",
|
||||
str(self).rstrip("\n"),
|
||||
@@ -290,7 +309,7 @@ class RawMessage(Message):
|
||||
tocall=self.tocall,
|
||||
fromcall=self.fromcall,
|
||||
)
|
||||
cl.sendall(str(self))
|
||||
cl.send(self)
|
||||
stats.APRSDStats().msgs_sent_inc()
|
||||
|
||||
|
||||
@@ -299,8 +318,16 @@ class TextMessage(Message):
|
||||
|
||||
message = None
|
||||
|
||||
def __init__(self, fromcall, tocall, message, msg_id=None, allow_delay=True):
|
||||
super().__init__(fromcall, tocall, msg_id)
|
||||
def __init__(
|
||||
self,
|
||||
fromcall,
|
||||
tocall,
|
||||
message,
|
||||
msg_id=None,
|
||||
allow_delay=True,
|
||||
transport=MESSAGE_TRANSPORT_APRSIS,
|
||||
):
|
||||
super().__init__(fromcall, tocall, msg_id, transport=transport)
|
||||
self.message = message
|
||||
# do we try and save this message for later if we don't get
|
||||
# an ack? Some messages we don't want to do this ever.
|
||||
@@ -352,9 +379,12 @@ class TextMessage(Message):
|
||||
thread = SendMessageThread(message=self)
|
||||
thread.start()
|
||||
|
||||
def send_direct(self):
|
||||
def send_direct(self, aprsis_client=None):
|
||||
"""Send a message without a separate thread."""
|
||||
cl = client.get_client()
|
||||
if aprsis_client:
|
||||
cl = aprsis_client
|
||||
else:
|
||||
cl = self.get_transport()
|
||||
log_message(
|
||||
"Sending Message Direct",
|
||||
str(self).rstrip("\n"),
|
||||
@@ -362,8 +392,9 @@ class TextMessage(Message):
|
||||
tocall=self.tocall,
|
||||
fromcall=self.fromcall,
|
||||
)
|
||||
cl.sendall(str(self))
|
||||
cl.send(self)
|
||||
stats.APRSDStats().msgs_tx_inc()
|
||||
packets.PacketList().add(self.dict())
|
||||
|
||||
|
||||
class SendMessageThread(threads.APRSDThread):
|
||||
@@ -382,7 +413,6 @@ class SendMessageThread(threads.APRSDThread):
|
||||
last send attempt is old enough.
|
||||
|
||||
"""
|
||||
cl = client.get_client()
|
||||
tracker = MsgTrack()
|
||||
# lets see if the message is still in the tracking queue
|
||||
msg = tracker.get(self.msg.id)
|
||||
@@ -392,6 +422,7 @@ class SendMessageThread(threads.APRSDThread):
|
||||
LOG.info("Message Send Complete via Ack.")
|
||||
return False
|
||||
else:
|
||||
cl = msg.get_transport()
|
||||
send_now = False
|
||||
if msg.last_send_attempt == msg.retry_count:
|
||||
# we reached the send limit, don't send again
|
||||
@@ -422,7 +453,7 @@ class SendMessageThread(threads.APRSDThread):
|
||||
retry_number=msg.last_send_attempt,
|
||||
msg_num=msg.id,
|
||||
)
|
||||
cl.sendall(str(msg))
|
||||
cl.send(msg)
|
||||
stats.APRSDStats().msgs_tx_inc()
|
||||
packets.PacketList().add(msg.dict())
|
||||
msg.last_send_time = datetime.datetime.now()
|
||||
@@ -436,8 +467,8 @@ class SendMessageThread(threads.APRSDThread):
|
||||
class AckMessage(Message):
|
||||
"""Class for building Acks and sending them."""
|
||||
|
||||
def __init__(self, fromcall, tocall, msg_id):
|
||||
super().__init__(fromcall, tocall, msg_id=msg_id)
|
||||
def __init__(self, fromcall, tocall, msg_id, transport=MESSAGE_TRANSPORT_APRSIS):
|
||||
super().__init__(fromcall, tocall, msg_id=msg_id, transport=transport)
|
||||
|
||||
def dict(self):
|
||||
now = datetime.datetime.now()
|
||||
@@ -463,14 +494,20 @@ class AckMessage(Message):
|
||||
self.id,
|
||||
)
|
||||
|
||||
def _filter_for_send(self):
|
||||
return f"ack{self.id}"
|
||||
|
||||
def send(self):
|
||||
LOG.debug(f"Send ACK({self.tocall}:{self.id}) to radio.")
|
||||
thread = SendAckThread(self)
|
||||
thread.start()
|
||||
|
||||
def send_direct(self):
|
||||
def send_direct(self, aprsis_client=None):
|
||||
"""Send an ack message without a separate thread."""
|
||||
cl = client.get_client()
|
||||
if aprsis_client:
|
||||
cl = aprsis_client
|
||||
else:
|
||||
cl = self.get_transport()
|
||||
log_message(
|
||||
"Sending ack",
|
||||
str(self).rstrip("\n"),
|
||||
@@ -479,7 +516,7 @@ class AckMessage(Message):
|
||||
tocall=self.tocall,
|
||||
fromcall=self.fromcall,
|
||||
)
|
||||
cl.sendall(str(self))
|
||||
cl.send(self)
|
||||
|
||||
|
||||
class SendAckThread(threads.APRSDThread):
|
||||
@@ -515,7 +552,7 @@ class SendAckThread(threads.APRSDThread):
|
||||
send_now = True
|
||||
|
||||
if send_now:
|
||||
cl = client.get_client()
|
||||
cl = self.ack.get_transport()
|
||||
log_message(
|
||||
"Sending ack",
|
||||
str(self.ack).rstrip("\n"),
|
||||
@@ -524,7 +561,7 @@ class SendAckThread(threads.APRSDThread):
|
||||
tocall=self.ack.tocall,
|
||||
retry_number=self.ack.last_send_attempt,
|
||||
)
|
||||
cl.sendall(str(self.ack))
|
||||
cl.send(self.ack)
|
||||
stats.APRSDStats().ack_tx_inc()
|
||||
packets.PacketList().add(self.ack.dict())
|
||||
self.ack.last_send_attempt += 1
|
||||
|
||||
+2
-1
@@ -69,6 +69,7 @@ class WatchList:
|
||||
|
||||
_instance = None
|
||||
callsigns = {}
|
||||
config = None
|
||||
|
||||
def __new__(cls, *args, **kwargs):
|
||||
if cls._instance is None:
|
||||
@@ -97,7 +98,7 @@ class WatchList:
|
||||
}
|
||||
|
||||
def is_enabled(self):
|
||||
if "watch_list" in self.config["aprsd"]:
|
||||
if self.config and "watch_list" in self.config["aprsd"]:
|
||||
return self.config["aprsd"]["watch_list"].get("enabled", False)
|
||||
else:
|
||||
return False
|
||||
|
||||
+30
-15
@@ -55,17 +55,21 @@ class APRSDPluginBase(metaclass=abc.ABCMeta):
|
||||
|
||||
# Holds the list of APRSDThreads that the plugin creates
|
||||
threads = []
|
||||
# Set this in setup()
|
||||
enabled = False
|
||||
|
||||
def __init__(self, config):
|
||||
self.config = config
|
||||
self.message_counter = 0
|
||||
self.setup()
|
||||
self.threads = self.create_threads()
|
||||
threads = self.create_threads()
|
||||
if threads:
|
||||
self.threads = threads
|
||||
if self.threads:
|
||||
self.start_threads()
|
||||
|
||||
def start_threads(self):
|
||||
if self.threads:
|
||||
if self.enabled and self.threads:
|
||||
if not isinstance(self.threads, list):
|
||||
self.threads = [self.threads]
|
||||
|
||||
@@ -97,8 +101,10 @@ class APRSDPluginBase(metaclass=abc.ABCMeta):
|
||||
"""Version"""
|
||||
raise NotImplementedError
|
||||
|
||||
@abc.abstractmethod
|
||||
def setup(self):
|
||||
"""Do any plugin setup here."""
|
||||
self.enabled = True
|
||||
|
||||
def create_threads(self):
|
||||
"""Gives the plugin writer the ability start a background thread."""
|
||||
@@ -137,7 +143,6 @@ class APRSDWatchListPluginBase(APRSDPluginBase, metaclass=abc.ABCMeta):
|
||||
by a particular HAM callsign, write a plugin based off of
|
||||
this class.
|
||||
"""
|
||||
enabled = False
|
||||
|
||||
def setup(self):
|
||||
# if we have a watch list enabled, we need to add filtering
|
||||
@@ -160,15 +165,18 @@ class APRSDWatchListPluginBase(APRSDPluginBase, metaclass=abc.ABCMeta):
|
||||
LOG.warning("Watch list enabled, but no callsigns set.")
|
||||
|
||||
def filter(self, packet):
|
||||
wl = packets.WatchList()
|
||||
result = messaging.NULL_MESSAGE
|
||||
if wl.callsign_in_watchlist(packet["from"]):
|
||||
# packet is from a callsign in the watch list
|
||||
self.rx_inc()
|
||||
result = self.process()
|
||||
if result:
|
||||
self.tx_inc()
|
||||
wl.update_seen(packet)
|
||||
if self.enabled:
|
||||
wl = packets.WatchList()
|
||||
result = messaging.NULL_MESSAGE
|
||||
if wl.callsign_in_watchlist(packet["from"]):
|
||||
# packet is from a callsign in the watch list
|
||||
self.rx_inc()
|
||||
result = self.process()
|
||||
if result:
|
||||
self.tx_inc()
|
||||
wl.update_seen(packet)
|
||||
else:
|
||||
LOG.warning(f"{self.__class__} plugin is not enabled")
|
||||
|
||||
return result
|
||||
|
||||
@@ -191,6 +199,10 @@ class APRSDRegexCommandPluginBase(APRSDPluginBase, metaclass=abc.ABCMeta):
|
||||
"""The regex to match from the caller"""
|
||||
raise NotImplementedError
|
||||
|
||||
def setup(self):
|
||||
"""Do any plugin setup here."""
|
||||
self.enabled = True
|
||||
|
||||
@hookimpl
|
||||
def filter(self, packet):
|
||||
result = None
|
||||
@@ -208,9 +220,12 @@ class APRSDRegexCommandPluginBase(APRSDPluginBase, metaclass=abc.ABCMeta):
|
||||
):
|
||||
if re.search(self.command_regex, message):
|
||||
self.rx_inc()
|
||||
result = self.process(packet)
|
||||
if result:
|
||||
self.tx_inc()
|
||||
if self.enabled:
|
||||
result = self.process(packet)
|
||||
if result:
|
||||
self.tx_inc()
|
||||
else:
|
||||
LOG.warning(f"{self.__class__} isn't enabled.")
|
||||
|
||||
return result
|
||||
|
||||
|
||||
@@ -143,7 +143,6 @@ class EmailPlugin(plugin.APRSDRegexCommandPluginBase):
|
||||
return reply
|
||||
|
||||
|
||||
@trace.trace
|
||||
def _imap_connect():
|
||||
global CONFIG
|
||||
imap_port = CONFIG["aprsd"]["email"]["imap"].get("port", 143)
|
||||
@@ -184,7 +183,6 @@ def _imap_connect():
|
||||
return server
|
||||
|
||||
|
||||
@trace.trace
|
||||
def _smtp_connect():
|
||||
host = CONFIG["aprsd"]["email"]["smtp"]["host"]
|
||||
smtp_port = CONFIG["aprsd"]["email"]["smtp"]["port"]
|
||||
@@ -248,14 +246,14 @@ def validate_shortcuts(config):
|
||||
LOG.info(f"Validating {key}:{shortcuts[key]}")
|
||||
is_valid = validate_email(
|
||||
email_address=shortcuts[key],
|
||||
check_regex=True,
|
||||
check_mx=False,
|
||||
from_address=config["aprsd"]["email"]["smtp"]["login"],
|
||||
helo_host=config["aprsd"]["email"]["smtp"]["host"],
|
||||
check_format=True,
|
||||
check_dns=True,
|
||||
check_smtp=True,
|
||||
smtp_from_address=config["aprsd"]["email"]["smtp"]["login"],
|
||||
smtp_helo_host=config["aprsd"]["email"]["smtp"]["host"],
|
||||
smtp_timeout=10,
|
||||
dns_timeout=10,
|
||||
use_blacklist=True,
|
||||
debug=False,
|
||||
smtp_debug=False,
|
||||
)
|
||||
if not is_valid:
|
||||
LOG.error(
|
||||
|
||||
@@ -306,7 +306,7 @@ class AVWXWeatherPlugin(plugin.APRSDRegexCommandPluginBase):
|
||||
"""
|
||||
|
||||
version = "1.0"
|
||||
command_regex = "^[metar]"
|
||||
command_regex = "^[mM]"
|
||||
command_name = "Weather"
|
||||
|
||||
@trace.trace
|
||||
@@ -314,7 +314,7 @@ class AVWXWeatherPlugin(plugin.APRSDRegexCommandPluginBase):
|
||||
fromcall = packet.get("from")
|
||||
message = packet.get("message_text", None)
|
||||
# ack = packet.get("msgNo", "0")
|
||||
LOG.info(f"OWMWeather Plugin '{message}'")
|
||||
LOG.info(f"AVWXWeather Plugin '{message}'")
|
||||
a = re.search(r"^.*\s+(.*)", message)
|
||||
if a is not None:
|
||||
searchcall = a.group(1)
|
||||
|
||||
@@ -194,6 +194,7 @@ class APRSDStats:
|
||||
|
||||
for p in plugins:
|
||||
plugin_stats[full_name_with_qualname(p)] = {
|
||||
"enabled": p.enabled,
|
||||
"rx": p.rx_count,
|
||||
"tx": p.tx_count,
|
||||
}
|
||||
|
||||
+102
-10
@@ -8,7 +8,7 @@ import tracemalloc
|
||||
|
||||
import aprslib
|
||||
|
||||
from aprsd import client, messaging, packets, plugin, stats, utils
|
||||
from aprsd import client, kissclient, messaging, packets, plugin, stats, utils
|
||||
|
||||
|
||||
LOG = logging.getLogger("APRSD")
|
||||
@@ -17,6 +17,7 @@ RX_THREAD = "RX"
|
||||
EMAIL_THREAD = "Email"
|
||||
|
||||
rx_msg_queue = queue.Queue(maxsize=20)
|
||||
logging_queue = queue.Queue()
|
||||
msg_queues = {
|
||||
"rx": rx_msg_queue,
|
||||
}
|
||||
@@ -88,6 +89,8 @@ class KeepAliveThread(APRSDThread):
|
||||
tracemalloc.start()
|
||||
super().__init__("KeepAlive")
|
||||
self.config = config
|
||||
max_timeout = {"hours": 0.0, "minutes": 5, "seconds": 0}
|
||||
self.max_delta = datetime.timedelta(**max_timeout)
|
||||
|
||||
def loop(self):
|
||||
if self.cntr % 60 == 0:
|
||||
@@ -125,6 +128,19 @@ class KeepAliveThread(APRSDThread):
|
||||
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 kissclient.KISSClient.kiss_enabled(self.config):
|
||||
LOG.warning("Resetting connection to APRS-IS.")
|
||||
client.Client().reset()
|
||||
|
||||
# Check version every hour
|
||||
delta = now - self.checker_time
|
||||
if delta > datetime.timedelta(hours=1):
|
||||
@@ -180,9 +196,10 @@ class APRSDRXThread(APRSDThread):
|
||||
|
||||
class APRSDProcessPacketThread(APRSDThread):
|
||||
|
||||
def __init__(self, packet, config):
|
||||
def __init__(self, packet, config, transport="aprsis"):
|
||||
self.packet = packet
|
||||
self.config = config
|
||||
self.transport = transport
|
||||
name = self.packet["raw"][:10]
|
||||
super().__init__(f"RX_PACKET-{name}")
|
||||
|
||||
@@ -237,6 +254,7 @@ class APRSDProcessPacketThread(APRSDThread):
|
||||
self.config["aprs"]["login"],
|
||||
fromcall,
|
||||
msg_id=msg_id,
|
||||
transport=self.transport,
|
||||
)
|
||||
ack.send()
|
||||
|
||||
@@ -250,14 +268,21 @@ class APRSDProcessPacketThread(APRSDThread):
|
||||
replied = True
|
||||
for subreply in reply:
|
||||
LOG.debug(f"Sending '{subreply}'")
|
||||
|
||||
msg = messaging.TextMessage(
|
||||
self.config["aprs"]["login"],
|
||||
fromcall,
|
||||
subreply,
|
||||
)
|
||||
msg.send()
|
||||
|
||||
if isinstance(subreply, messaging.Message):
|
||||
subreply.send()
|
||||
else:
|
||||
msg = messaging.TextMessage(
|
||||
self.config["aprs"]["login"],
|
||||
fromcall,
|
||||
subreply,
|
||||
transport=self.transport,
|
||||
)
|
||||
msg.send()
|
||||
elif isinstance(reply, messaging.Message):
|
||||
# We have a message based object.
|
||||
LOG.debug(f"Sending '{reply}'")
|
||||
reply.send()
|
||||
replied = True
|
||||
else:
|
||||
replied = True
|
||||
# A plugin can return a null message flag which signals
|
||||
@@ -271,6 +296,7 @@ class APRSDProcessPacketThread(APRSDThread):
|
||||
self.config["aprs"]["login"],
|
||||
fromcall,
|
||||
reply,
|
||||
transport=self.transport,
|
||||
)
|
||||
msg.send()
|
||||
|
||||
@@ -283,6 +309,7 @@ class APRSDProcessPacketThread(APRSDThread):
|
||||
self.config["aprs"]["login"],
|
||||
fromcall,
|
||||
reply,
|
||||
transport=self.transport,
|
||||
)
|
||||
msg.send()
|
||||
except Exception as ex:
|
||||
@@ -294,6 +321,7 @@ class APRSDProcessPacketThread(APRSDThread):
|
||||
self.config["aprs"]["login"],
|
||||
fromcall,
|
||||
reply,
|
||||
transport=self.transport,
|
||||
)
|
||||
msg.send()
|
||||
|
||||
@@ -314,3 +342,67 @@ class APRSDTXThread(APRSDThread):
|
||||
pass
|
||||
# Continue to loop
|
||||
return True
|
||||
|
||||
|
||||
class KISSRXThread(APRSDThread):
|
||||
"""Thread that connects to direwolf's TCPKISS interface.
|
||||
|
||||
All Packets are processed and sent back out the direwolf
|
||||
interface instead of the aprs-is server.
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, msg_queues, config):
|
||||
super().__init__("KISSRX_MSG")
|
||||
self.msg_queues = msg_queues
|
||||
self.config = config
|
||||
|
||||
def stop(self):
|
||||
self.thread_stop = True
|
||||
kissclient.get_client().stop()
|
||||
|
||||
def loop(self):
|
||||
kiss_client = kissclient.get_client()
|
||||
|
||||
# setup the consumer of messages and block until a messages
|
||||
try:
|
||||
# This will register a packet consumer with aprslib
|
||||
# When new packets come in the consumer will process
|
||||
# the packet
|
||||
|
||||
# Do a partial here because the consumer signature doesn't allow
|
||||
# For kwargs to be passed in to the consumer func we declare
|
||||
# and the aprslib developer didn't want to allow a PR to add
|
||||
# kwargs. :(
|
||||
# https://github.com/rossengeorgiev/aprs-python/pull/56
|
||||
kiss_client.consumer(self.process_packet, callsign=self.config["kiss"]["callsign"])
|
||||
kiss_client.loop.run_forever()
|
||||
|
||||
except aprslib.exceptions.ConnectionDrop:
|
||||
LOG.error("Connection dropped, reconnecting")
|
||||
time.sleep(5)
|
||||
# Force the deletion of the client object connected to aprs
|
||||
# This will cause a reconnect, next time client.get_client()
|
||||
# is called
|
||||
client.Client().reset()
|
||||
# Continue to loop
|
||||
|
||||
def process_packet(self, interface, frame):
|
||||
"""Process a packet recieved from aprs-is server."""
|
||||
|
||||
LOG.debug(f"Got an APRS Frame '{frame}'")
|
||||
# try and nuke the * from the fromcall sign.
|
||||
frame.header._source._ch = False
|
||||
payload = str(frame.payload.decode())
|
||||
msg = f"{str(frame.header)}:{payload}"
|
||||
# msg = frame.tnc2
|
||||
LOG.debug(f"Decoding {msg}")
|
||||
|
||||
packet = aprslib.parse(msg)
|
||||
LOG.debug(packet)
|
||||
thread = APRSDProcessPacketThread(
|
||||
packet=packet, config=self.config,
|
||||
transport=messaging.MESSAGE_TRANSPORT_TCPKISS,
|
||||
)
|
||||
thread.start()
|
||||
return
|
||||
|
||||
+25
-4
@@ -26,22 +26,40 @@ LOG_LEVELS = {
|
||||
"DEBUG": logging.DEBUG,
|
||||
}
|
||||
|
||||
DEFAULT_DATE_FORMAT = "%m/%d/%Y %I:%M:%S %p"
|
||||
DEFAULT_LOG_FORMAT = (
|
||||
"[%(asctime)s] [%(threadName)-12s] [%(levelname)-5.5s]"
|
||||
"[%(asctime)s] [%(threadName)-20.20s] [%(levelname)-5.5s]"
|
||||
" %(message)s - [%(pathname)s:%(lineno)d]"
|
||||
)
|
||||
|
||||
DEFAULT_DATE_FORMAT = "%m/%d/%Y %I:%M:%S %p"
|
||||
QUEUE_DATE_FORMAT = "[%m/%d/%Y] [%I:%M:%S %p]"
|
||||
QUEUE_LOG_FORMAT = (
|
||||
"%(asctime)s [%(threadName)-20.20s] [%(levelname)-5.5s]"
|
||||
" %(message)s - [%(pathname)s:%(lineno)d]"
|
||||
)
|
||||
|
||||
# an example of what should be in the ~/.aprsd/config.yml
|
||||
DEFAULT_CONFIG_DICT = {
|
||||
"ham": {"callsign": "NOCALL"},
|
||||
"aprs": {
|
||||
"login": "NOCALL",
|
||||
"enabled": True,
|
||||
"login": "CALLSIGN",
|
||||
"password": "00000",
|
||||
"host": "rotate.aprs2.net",
|
||||
"port": 14580,
|
||||
},
|
||||
"kiss": {
|
||||
"tcp": {
|
||||
"enabled": False,
|
||||
"host": "direwolf.ip.address",
|
||||
"port": "8001",
|
||||
},
|
||||
"serial": {
|
||||
"enabled": False,
|
||||
"device": "/dev/ttyS0",
|
||||
"baudrate": 9600,
|
||||
},
|
||||
},
|
||||
"aprsd": {
|
||||
"logfile": "/tmp/aprsd.log",
|
||||
"logformat": DEFAULT_LOG_FORMAT,
|
||||
@@ -172,6 +190,9 @@ def add_config_comments(raw_yaml):
|
||||
# lets insert a comment
|
||||
raw_yaml = insert_str(
|
||||
raw_yaml,
|
||||
"\n # Set enabled to False if there is no internet connectivity."
|
||||
"\n # This is useful for a direwolf KISS aprs connection only. "
|
||||
"\n"
|
||||
"\n # Get the passcode for your callsign here: "
|
||||
"\n # https://apps.magicbug.co.uk/passcode",
|
||||
end_idx,
|
||||
@@ -272,7 +293,7 @@ def conf_option_exists(conf, chain):
|
||||
|
||||
def check_config_option(config, chain, default_fail=None):
|
||||
result = conf_option_exists(config, chain.copy())
|
||||
if not result:
|
||||
if result is None:
|
||||
raise Exception(
|
||||
"'{}' was not in config file".format(
|
||||
chain,
|
||||
|
||||
@@ -0,0 +1,189 @@
|
||||
/* PrismJS 1.24.1
|
||||
https://prismjs.com/download.html#themes=prism-tomorrow&languages=markup+css+clike+javascript+log&plugins=show-language+toolbar */
|
||||
/**
|
||||
* prism.js tomorrow night eighties for JavaScript, CoffeeScript, CSS and HTML
|
||||
* Based on https://github.com/chriskempson/tomorrow-theme
|
||||
* @author Rose Pritchard
|
||||
*/
|
||||
|
||||
code[class*="language-"],
|
||||
pre[class*="language-"] {
|
||||
color: #ccc;
|
||||
background: none;
|
||||
font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace;
|
||||
font-size: 1em;
|
||||
text-align: left;
|
||||
white-space: pre;
|
||||
word-spacing: normal;
|
||||
word-break: normal;
|
||||
word-wrap: normal;
|
||||
line-height: 1.5;
|
||||
|
||||
-moz-tab-size: 4;
|
||||
-o-tab-size: 4;
|
||||
tab-size: 4;
|
||||
|
||||
-webkit-hyphens: none;
|
||||
-moz-hyphens: none;
|
||||
-ms-hyphens: none;
|
||||
hyphens: none;
|
||||
|
||||
}
|
||||
|
||||
/* Code blocks */
|
||||
pre[class*="language-"] {
|
||||
padding: 1em;
|
||||
margin: .5em 0;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
:not(pre) > code[class*="language-"],
|
||||
pre[class*="language-"] {
|
||||
background: #2d2d2d;
|
||||
}
|
||||
|
||||
/* Inline code */
|
||||
:not(pre) > code[class*="language-"] {
|
||||
padding: .1em;
|
||||
border-radius: .3em;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.token.comment,
|
||||
.token.block-comment,
|
||||
.token.prolog,
|
||||
.token.doctype,
|
||||
.token.cdata {
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.token.punctuation {
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
.token.tag,
|
||||
.token.attr-name,
|
||||
.token.namespace,
|
||||
.token.deleted {
|
||||
color: #e2777a;
|
||||
}
|
||||
|
||||
.token.function-name {
|
||||
color: #6196cc;
|
||||
}
|
||||
|
||||
.token.boolean,
|
||||
.token.number,
|
||||
.token.function {
|
||||
color: #f08d49;
|
||||
}
|
||||
|
||||
.token.property,
|
||||
.token.class-name,
|
||||
.token.constant,
|
||||
.token.symbol {
|
||||
color: #f8c555;
|
||||
}
|
||||
|
||||
.token.selector,
|
||||
.token.important,
|
||||
.token.atrule,
|
||||
.token.keyword,
|
||||
.token.builtin {
|
||||
color: #cc99cd;
|
||||
}
|
||||
|
||||
.token.string,
|
||||
.token.char,
|
||||
.token.attr-value,
|
||||
.token.regex,
|
||||
.token.variable {
|
||||
color: #7ec699;
|
||||
}
|
||||
|
||||
.token.operator,
|
||||
.token.entity,
|
||||
.token.url {
|
||||
color: #67cdcc;
|
||||
}
|
||||
|
||||
.token.important,
|
||||
.token.bold {
|
||||
font-weight: bold;
|
||||
}
|
||||
.token.italic {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.token.entity {
|
||||
cursor: help;
|
||||
}
|
||||
|
||||
.token.inserted {
|
||||
color: green;
|
||||
}
|
||||
|
||||
div.code-toolbar {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
div.code-toolbar > .toolbar {
|
||||
position: absolute;
|
||||
top: .3em;
|
||||
right: .2em;
|
||||
transition: opacity 0.3s ease-in-out;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
div.code-toolbar:hover > .toolbar {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* Separate line b/c rules are thrown out if selector is invalid.
|
||||
IE11 and old Edge versions don't support :focus-within. */
|
||||
div.code-toolbar:focus-within > .toolbar {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
div.code-toolbar > .toolbar > .toolbar-item {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
div.code-toolbar > .toolbar > .toolbar-item > a {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
div.code-toolbar > .toolbar > .toolbar-item > button {
|
||||
background: none;
|
||||
border: 0;
|
||||
color: inherit;
|
||||
font: inherit;
|
||||
line-height: normal;
|
||||
overflow: visible;
|
||||
padding: 0;
|
||||
-webkit-user-select: none; /* for button */
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
}
|
||||
|
||||
div.code-toolbar > .toolbar > .toolbar-item > a,
|
||||
div.code-toolbar > .toolbar > .toolbar-item > button,
|
||||
div.code-toolbar > .toolbar > .toolbar-item > span {
|
||||
color: #bbb;
|
||||
font-size: .8em;
|
||||
padding: 0 .5em;
|
||||
background: #f5f2f0;
|
||||
background: rgba(224, 224, 224, 0.2);
|
||||
box-shadow: 0 2px 0 0 rgba(0,0,0,0.2);
|
||||
border-radius: .5em;
|
||||
}
|
||||
|
||||
div.code-toolbar > .toolbar > .toolbar-item > a:hover,
|
||||
div.code-toolbar > .toolbar > .toolbar-item > a:focus,
|
||||
div.code-toolbar > .toolbar > .toolbar-item > button:hover,
|
||||
div.code-toolbar > .toolbar > .toolbar-item > button:focus,
|
||||
div.code-toolbar > .toolbar > .toolbar-item > span:hover,
|
||||
div.code-toolbar > .toolbar > .toolbar-item > span:focus {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
@@ -220,7 +220,7 @@ function updateQuadData(chart, label, first, second, third, fourth) {
|
||||
|
||||
function update_stats( data ) {
|
||||
$("#version").text( data["stats"]["aprsd"]["version"] );
|
||||
$("#aprsis").html( "APRS-IS Server: <a href='http://status.aprs2.net' >" + data["stats"]["aprs-is"]["server"] + "</a>" );
|
||||
$("#aprs_connection").html( data["aprs_connection"] );
|
||||
$("#uptime").text( "uptime: " + data["stats"]["aprsd"]["uptime"] );
|
||||
const html_pretty = Prism.highlight(JSON.stringify(data, null, '\t'), Prism.languages.json, 'json');
|
||||
$("#jsonstats").html(html_pretty);
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
function init_logs() {
|
||||
const socket = io("/logs");
|
||||
socket.on('connect', function () {
|
||||
console.log("Connected to logs socketio");
|
||||
});
|
||||
|
||||
socket.on('connected', function(msg) {
|
||||
console.log("Connected to /logs");
|
||||
console.log(msg);
|
||||
});
|
||||
|
||||
socket.on('log_entry', function(data) {
|
||||
update_logs(data);
|
||||
});
|
||||
|
||||
};
|
||||
|
||||
|
||||
function update_logs(data) {
|
||||
var code_block = $('#logtext')
|
||||
entry = data["message"]
|
||||
const html_pretty = Prism.highlight(entry, Prism.languages.log, 'log');
|
||||
code_block.append(html_pretty + "<br>");
|
||||
var div = document.getElementById('logContainer');
|
||||
div.scrollTop = div.scrollHeight;
|
||||
}
|
||||
@@ -60,7 +60,10 @@ function update_watchlist_from_packet(callsign, val) {
|
||||
|
||||
function update_plugins( data ) {
|
||||
var plugindiv = $("#pluginDiv");
|
||||
var html_str = '<table class="ui celled striped table"><thead><tr><th>Plugin Name</th><th>Processed Packets</th><th>Sent Packets</th></tr></thead><tbody>'
|
||||
var html_str = '<table class="ui celled striped table"><thead><tr>'
|
||||
html_str += '<th>Plugin Name</th><th>Plugin Enabled?</th>'
|
||||
html_str += '<th>Processed Packets</th><th>Sent Packets</th>'
|
||||
html_str += '</tr></thead><tbody>'
|
||||
plugindiv.html('')
|
||||
|
||||
var plugins = data["stats"]["plugins"];
|
||||
@@ -69,7 +72,7 @@ function update_plugins( data ) {
|
||||
for (var i=0; i<keys.length; i++) { // now lets iterate in sort order
|
||||
var key = keys[i];
|
||||
var val = plugins[key];
|
||||
html_str += '<tr><td class="collapsing">' + key + '</td><td>' + val["rx"] + '</td><td>' + val["tx"] + '</td></tr>';
|
||||
html_str += '<tr><td class="collapsing">' + key + '</td><td>' + val["enabled"] + '</td><td>' + val["rx"] + '</td><td>' + val["tx"] + '</td></tr>';
|
||||
}
|
||||
html_str += "</tbody></table>";
|
||||
plugindiv.append(html_str);
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,144 @@
|
||||
var cleared = false;
|
||||
|
||||
function size_dict(d){c=0; for (i in d) ++c; return c}
|
||||
|
||||
function init_messages() {
|
||||
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 = $("#msgsDiv");
|
||||
msgsdiv.html('')
|
||||
cleared = true
|
||||
}
|
||||
add_msg(msg);
|
||||
});
|
||||
|
||||
socket.on("ack", function(msg) {
|
||||
update_msg(msg);
|
||||
});
|
||||
socket.on("reply", function(msg) {
|
||||
update_msg(msg);
|
||||
});
|
||||
|
||||
$("#sendform").submit(function(event) {
|
||||
event.preventDefault();
|
||||
|
||||
var $checkboxes = $(this).find('input[type=checkbox]');
|
||||
|
||||
//loop through the checkboxes and change to hidden fields
|
||||
$checkboxes.each(function() {
|
||||
if ($(this)[0].checked) {
|
||||
$(this).attr('type', 'hidden');
|
||||
$(this).val(1);
|
||||
} else {
|
||||
$(this).attr('type', 'hidden');
|
||||
$(this).val(0);
|
||||
}
|
||||
});
|
||||
|
||||
msg = {'from': $('#from').val(),
|
||||
'password': $('#password').val(),
|
||||
'to': $('#to').val(),
|
||||
'message': $('#message').val(),
|
||||
'wait_reply': $('#wait_reply').val(),
|
||||
}
|
||||
|
||||
socket.emit("send", msg);
|
||||
|
||||
//loop through the checkboxes and change to hidden fields
|
||||
$checkboxes.each(function() {
|
||||
$(this).attr('type', 'checkbox');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function add_msg(msg) {
|
||||
var msgsdiv = $("#sendMsgsDiv");
|
||||
|
||||
ts_str = msg["ts"].toString();
|
||||
ts = ts_str.split(".")[0]*1000;
|
||||
var d = new Date(ts).toLocaleDateString("en-US")
|
||||
var t = new Date(ts).toLocaleTimeString("en-US")
|
||||
|
||||
from = msg['from']
|
||||
title_id = 'title_tx'
|
||||
var from_to = d + " " + t + " " + from + " > "
|
||||
|
||||
if (msg.hasOwnProperty('to')) {
|
||||
from_to = from_to + msg['to']
|
||||
}
|
||||
from_to = from_to + " - " + msg['message']
|
||||
|
||||
id = ts_str.split('.')[0]
|
||||
pretty_id = "pretty_" + id
|
||||
loader_id = "loader_" + id
|
||||
ack_id = "ack_" + id
|
||||
reply_id = "reply_" + id
|
||||
span_id = "span_" + id
|
||||
json_pretty = Prism.highlight(JSON.stringify(msg, null, '\t'), Prism.languages.json, 'json');
|
||||
msg_html = '<div class="ui title" id="' + title_id + '"><i class="dropdown icon"></i>';
|
||||
msg_html += '<div class="ui active inline loader" id="' + loader_id +'" data-content="Waiting for Ack"></div> ';
|
||||
msg_html += '<i class="thumbs down outline icon" id="' + ack_id + '" data-content="Waiting for ACK"></i> ';
|
||||
msg_html += '<i class="thumbs down outline icon" id="' + reply_id + '" data-content="Waiting for Reply"></i> ';
|
||||
msg_html += '<span id="' + span_id + '">' + from_to +'</span></div>';
|
||||
msg_html += '<div class="content"><p class="transition hidden"><pre id="' + pretty_id + '" class="language-json">' + json_pretty + '</p></p></div>'
|
||||
msgsdiv.prepend(msg_html);
|
||||
$('.ui.accordion').accordion('refresh');
|
||||
}
|
||||
|
||||
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
|
||||
reply_id = "reply_" + 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');
|
||||
}
|
||||
|
||||
if (msg['reply'] !== null) {
|
||||
var reply_div = $('#' + reply_id);
|
||||
reply_div.removeClass("thumbs down outline icon");
|
||||
reply_div.addClass('reply icon');
|
||||
reply_div.attr('data-content', 'Got Reply');
|
||||
|
||||
var d = new Date(ts).toLocaleDateString("en-US")
|
||||
var t = new Date(ts).toLocaleTimeString("en-US")
|
||||
var from_to = d + " " + t + " " + from + " > "
|
||||
|
||||
if (msg.hasOwnProperty('to')) {
|
||||
from_to = from_to + msg['to']
|
||||
}
|
||||
from_to = from_to + " - " + msg['message']
|
||||
from_to += " ===> " + msg["reply"]["message_text"]
|
||||
|
||||
var span_div = $('#' + span_id);
|
||||
span_div.html(from_to);
|
||||
}
|
||||
|
||||
var pretty_pre = $("#" + pretty_id);
|
||||
pretty_pre.html('');
|
||||
json_pretty = Prism.highlight(JSON.stringify(msg, null, '\t'), Prism.languages.json, 'json');
|
||||
pretty_pre.html(json_pretty);
|
||||
$('.ui.accordion').accordion('refresh');
|
||||
}
|
||||
@@ -3,10 +3,8 @@
|
||||
<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>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/prismjs@1.23.0/prism.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/prismjs@1.23.0/components/prism-json.js"></script>
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/prismjs@1.23.0/themes/prism-tomorrow.css">
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js@2.9.4/dist/Chart.bundle.js"></script>
|
||||
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/semantic-ui@2.4.2/dist/semantic.min.css">
|
||||
@@ -14,12 +12,17 @@
|
||||
|
||||
<link rel="stylesheet" href="/static/css/index.css">
|
||||
<link rel="stylesheet" href="/static/css/tabs.css">
|
||||
<link rel="stylesheet" href="/static/css/prism.css">
|
||||
<script src="/static/js/prism.js"></script>
|
||||
<script src="/static/js/main.js"></script>
|
||||
<script src="/static/js/charts.js"></script>
|
||||
<script src="/static/js/tabs.js"></script>
|
||||
<script src="/static/js/send-message.js"></script>
|
||||
<script src="/static/js/packets.js"></script>
|
||||
<script src="/static/js/logs.js"></script>
|
||||
|
||||
|
||||
<script type="text/javascript"">
|
||||
<script type="text/javascript">
|
||||
var initial_stats = {{ initial_stats|tojson|safe }};
|
||||
|
||||
var memory_chart = null
|
||||
@@ -30,6 +33,8 @@
|
||||
console.log(initial_stats);
|
||||
start_update();
|
||||
start_charts();
|
||||
init_messages();
|
||||
init_logs();
|
||||
|
||||
$("#toggleStats").click(function() {
|
||||
$("#jsonstats").fadeToggle(1000);
|
||||
@@ -43,6 +48,10 @@
|
||||
$("#configjson").html(html_pretty);
|
||||
$("#jsonstats").fadeToggle(1000);
|
||||
|
||||
//var log_text_pretty = $('#logtext').text();
|
||||
//const log_pretty = Prism.highlight( log_text_pretty, Prism.languages.log, 'log');
|
||||
//$('#logtext').html(log_pretty);
|
||||
|
||||
$('.ui.accordion').accordion({exclusive: false});
|
||||
$('.menu .item').tab('change tab', 'charts-tab');
|
||||
});
|
||||
@@ -58,7 +67,7 @@
|
||||
<div class='left floated ten wide column'>
|
||||
<span style='color: green'>{{ callsign }}</span>
|
||||
connected to
|
||||
<span style='color: blue' id='aprsis'>NONE</span>
|
||||
<span style='color: blue' id='aprs_connection'>{{ aprs_connection|safe }}</span>
|
||||
</div>
|
||||
|
||||
<div class='right floated four wide column'>
|
||||
@@ -73,6 +82,8 @@
|
||||
<div class="item" data-tab="watch-tab">Watch List</div>
|
||||
<div class="item" data-tab="plugin-tab">Plugins</div>
|
||||
<div class="item" data-tab="config-tab">Config</div>
|
||||
<div class="item" data-tab="send-tab">Send Message</div>
|
||||
<div class="item" data-tab="log-tab">LogFile</div>
|
||||
<div class="item" data-tab="raw-tab">Raw JSON</div>
|
||||
</div>
|
||||
|
||||
@@ -142,6 +153,34 @@
|
||||
<pre id="configjson" class="language-json">{{ config_json|safe }}</pre>
|
||||
</div>
|
||||
|
||||
<div class="ui bottom attached tab segment" data-tab="send-tab">
|
||||
<h3 class="ui dividing header">Send Message</h3>
|
||||
<div id="sendMsgDiv" class="ui mini text">
|
||||
<form id="sendform" name="sendmsg" action="">
|
||||
<p><label for="from_call">From Callsign:</label>
|
||||
<input type="text" name="from_call" id="from"></p>
|
||||
<p><label for="from_call_password">Password:</label>
|
||||
<input type="password" name="from_call_password" id='password'></p>
|
||||
<p><label for="to_call">To Callsign:</label>
|
||||
<input type="text" name="to_call" id="to" ></p>
|
||||
<p><label for="message">Message:</label>
|
||||
<input type="text" name="message" id="message" ></p>
|
||||
<p><label for="wait">Wait for Reply?</label>
|
||||
<input type="checkbox" name="wait_reply" id="wait_reply" value="off" checked>
|
||||
</p>
|
||||
<input type="submit" name="submit" class="button" id="send_msg" value="Send" />
|
||||
</form>
|
||||
<div class="ui styled fluid accordion" id="accordion">
|
||||
<div id="sendMsgsDiv" class="ui mini text">Messages</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="ui bottom attached tab segment" data-tab="log-tab">
|
||||
<h3 class="ui dividing header">LOGFILE</h3>
|
||||
<pre id="logContainer" style="height: 600px;overflow-y:auto;overflow-x:auto;"><code id="logtext" class="language-log" ></code></pre>
|
||||
</div>
|
||||
|
||||
<div class="ui bottom attached tab segment" data-tab="raw-tab">
|
||||
<h3 class="ui dividing header">Raw JSON</h3>
|
||||
<pre id="jsonstats" class="language-json">{{ stats|safe }}</pre>
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
<html>
|
||||
<head>
|
||||
<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.jsdelivr.net/npm/jquery-simple-websocket@1.1.4/src/jquery.simple.websocket.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>
|
||||
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/prismjs@1.23.0/prism.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/prismjs@1.23.0/components/prism-json.js"></script>
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/prismjs@1.23.0/themes/prism-tomorrow.css">
|
||||
|
||||
<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/send-message.js"></script>
|
||||
|
||||
<script language="JavaScript">
|
||||
$(document).ready(function() {
|
||||
init_messages();
|
||||
});
|
||||
</script>
|
||||
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class='ui text container'>
|
||||
<h1 class='ui dividing header'>APRSD {{ 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='aprsis'>NONE</span>
|
||||
</div>
|
||||
|
||||
<div class='right floated four wide column'>
|
||||
<span id='uptime'>NONE</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3 class="ui dividing header">Send Message Form</h3>
|
||||
<form id="sendform" name="sendmsg" action="">
|
||||
<p><label for="from_call">From Callsign:</label>
|
||||
<input type="text" name="from_call" id="from" value="WB4BOR"></p>
|
||||
<p><label for="from_call_password">Password:</label>
|
||||
<input type="password" name="from_call_password" id='password' value="24496"></p>
|
||||
|
||||
<p><label for="to_call">To Callsign:</label>
|
||||
<input type="text" name="to_call" id="to" value="WB4BOR-11"></p>
|
||||
|
||||
<p><label for="message">Message:</label>
|
||||
<input type="text" name="message" id="message" value="ping"></p>
|
||||
|
||||
<p><label for="wait">Wait for Reply?</label>
|
||||
<input type="checkbox" name="wait_reply" id="wait_reply" value="off" checked>
|
||||
</p>
|
||||
|
||||
<input type="submit" name="submit" class="button" id="send_msg" value="Send" />
|
||||
</form>
|
||||
|
||||
<h3 class="ui dividing header">Messages (<span id="msgs_count">0</span>)</h3>
|
||||
<div class="ui styled fluid accordion" id="accordion">
|
||||
<div id="msgsDiv" class="ui mini text">Messages</div>
|
||||
</div>
|
||||
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
||||
+2
-2
@@ -1,12 +1,12 @@
|
||||
flake8
|
||||
isort
|
||||
mypy
|
||||
pytest
|
||||
pytest-cov
|
||||
pep8-naming
|
||||
Sphinx
|
||||
tox
|
||||
twine
|
||||
pre-commit
|
||||
pip-tools
|
||||
pytest
|
||||
pytest-cov
|
||||
gray
|
||||
|
||||
+61
-55
@@ -1,5 +1,5 @@
|
||||
#
|
||||
# This file is autogenerated by pip-compile
|
||||
# This file is autogenerated by pip-compile with python 3.8
|
||||
# To update, run:
|
||||
#
|
||||
# pip-compile dev-requirements.in
|
||||
@@ -9,10 +9,8 @@ add-trailing-comma==2.1.0
|
||||
alabaster==0.7.12
|
||||
# via sphinx
|
||||
appdirs==1.4.4
|
||||
# via
|
||||
# black
|
||||
# virtualenv
|
||||
attrs==20.3.0
|
||||
# via black
|
||||
attrs==21.2.0
|
||||
# via
|
||||
# jsonschema
|
||||
# pytest
|
||||
@@ -20,17 +18,19 @@ autoflake==1.4
|
||||
# via gray
|
||||
babel==2.9.1
|
||||
# via sphinx
|
||||
backports.entry-points-selectable==1.1.0
|
||||
# via virtualenv
|
||||
black==21.7b0
|
||||
# via gray
|
||||
bleach==3.3.0
|
||||
bleach==4.1.0
|
||||
# via readme-renderer
|
||||
certifi==2020.12.5
|
||||
certifi==2021.5.30
|
||||
# via requests
|
||||
cfgv==3.2.0
|
||||
cfgv==3.3.1
|
||||
# via pre-commit
|
||||
chardet==4.0.0
|
||||
charset-normalizer==2.0.4
|
||||
# via requests
|
||||
click==7.1.2
|
||||
click==8.0.1
|
||||
# via
|
||||
# black
|
||||
# pip-tools
|
||||
@@ -42,9 +42,9 @@ configargparse==1.5.2
|
||||
# via gray
|
||||
coverage==5.5
|
||||
# via pytest-cov
|
||||
distlib==0.3.1
|
||||
distlib==0.3.2
|
||||
# via virtualenv
|
||||
docutils==0.16
|
||||
docutils==0.17.1
|
||||
# via
|
||||
# readme-renderer
|
||||
# sphinx
|
||||
@@ -56,22 +56,23 @@ filelock==3.0.12
|
||||
# virtualenv
|
||||
fixit==0.1.4
|
||||
# via gray
|
||||
flake8-polyfill==1.0.2
|
||||
# via pep8-naming
|
||||
flake8==3.9.1
|
||||
flake8==3.9.2
|
||||
# via
|
||||
# -r dev-requirements.in
|
||||
# fixit
|
||||
# flake8-polyfill
|
||||
# pep8-naming
|
||||
flake8-polyfill==1.0.2
|
||||
# via pep8-naming
|
||||
gray==0.10.1
|
||||
# via -r dev-requirements.in
|
||||
identify==2.2.4
|
||||
identify==2.2.13
|
||||
# via pre-commit
|
||||
idna==2.10
|
||||
idna==3.2
|
||||
# via requests
|
||||
imagesize==1.2.0
|
||||
# via sphinx
|
||||
importlib-metadata==4.0.1
|
||||
importlib-metadata==4.7.1
|
||||
# via
|
||||
# keyring
|
||||
# twine
|
||||
@@ -79,52 +80,54 @@ importlib-resources==5.2.2
|
||||
# via fixit
|
||||
iniconfig==1.1.1
|
||||
# via pytest
|
||||
isort==5.8.0
|
||||
isort==5.9.3
|
||||
# via
|
||||
# -r dev-requirements.in
|
||||
# gray
|
||||
jinja2==2.11.3
|
||||
jinja2==3.0.1
|
||||
# via sphinx
|
||||
jsonschema==3.2.0
|
||||
# via fixit
|
||||
keyring==23.0.1
|
||||
keyring==23.1.0
|
||||
# via twine
|
||||
libcst==0.3.20
|
||||
# via fixit
|
||||
markupsafe==1.1.1
|
||||
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
|
||||
mypy==0.812
|
||||
# via -r dev-requirements.in
|
||||
nodeenv==1.6.0
|
||||
# via pre-commit
|
||||
packaging==20.9
|
||||
packaging==21.0
|
||||
# via
|
||||
# bleach
|
||||
# pytest
|
||||
# sphinx
|
||||
# tox
|
||||
pathspec==0.8.1
|
||||
pathspec==0.9.0
|
||||
# via black
|
||||
pep517==0.10.0
|
||||
pep517==0.11.0
|
||||
# via pip-tools
|
||||
pep8-naming==0.11.1
|
||||
pep8-naming==0.12.1
|
||||
# via -r dev-requirements.in
|
||||
pip-tools==6.1.0
|
||||
pip-tools==6.2.0
|
||||
# via -r dev-requirements.in
|
||||
pkginfo==1.7.0
|
||||
pkginfo==1.7.1
|
||||
# via twine
|
||||
pluggy==0.13.1
|
||||
platformdirs==2.2.0
|
||||
# via virtualenv
|
||||
pluggy==1.0.0
|
||||
# via
|
||||
# pytest
|
||||
# tox
|
||||
pre-commit==2.12.1
|
||||
pre-commit==2.14.0
|
||||
# via -r dev-requirements.in
|
||||
prettylog==0.3.0
|
||||
# via gray
|
||||
@@ -138,7 +141,7 @@ pyflakes==2.3.1
|
||||
# via
|
||||
# autoflake
|
||||
# flake8
|
||||
pygments==2.9.0
|
||||
pygments==2.10.0
|
||||
# via
|
||||
# readme-renderer
|
||||
# sphinx
|
||||
@@ -146,12 +149,12 @@ pyparsing==2.4.7
|
||||
# via packaging
|
||||
pyrsistent==0.18.0
|
||||
# via jsonschema
|
||||
pytest-cov==2.11.1
|
||||
# via -r dev-requirements.in
|
||||
pytest==6.2.3
|
||||
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
|
||||
@@ -163,18 +166,18 @@ pyyaml==5.4.1
|
||||
# pre-commit
|
||||
readme-renderer==29.0
|
||||
# via twine
|
||||
regex==2021.4.4
|
||||
regex==2021.8.27
|
||||
# via black
|
||||
requests-toolbelt==0.9.1
|
||||
# via twine
|
||||
requests==2.25.1
|
||||
requests==2.26.0
|
||||
# via
|
||||
# requests-toolbelt
|
||||
# sphinx
|
||||
# twine
|
||||
rfc3986==1.4.0
|
||||
requests-toolbelt==0.9.1
|
||||
# via twine
|
||||
six==1.15.0
|
||||
rfc3986==1.5.0
|
||||
# via twine
|
||||
six==1.16.0
|
||||
# via
|
||||
# bleach
|
||||
# jsonschema
|
||||
@@ -183,19 +186,19 @@ six==1.15.0
|
||||
# virtualenv
|
||||
snowballstemmer==2.1.0
|
||||
# via sphinx
|
||||
sphinx==3.5.4
|
||||
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==1.0.3
|
||||
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.4
|
||||
sphinxcontrib-serializinghtml==1.1.5
|
||||
# via sphinx
|
||||
tokenize-rt==4.1.0
|
||||
# via
|
||||
@@ -203,20 +206,21 @@ tokenize-rt==4.1.0
|
||||
# pyupgrade
|
||||
toml==0.10.2
|
||||
# via
|
||||
# pep517
|
||||
# mypy
|
||||
# pre-commit
|
||||
# pytest
|
||||
# pytest-cov
|
||||
# tox
|
||||
tomli==1.2.1
|
||||
# via black
|
||||
tox==3.23.0
|
||||
# via
|
||||
# black
|
||||
# pep517
|
||||
tox==3.24.3
|
||||
# via -r dev-requirements.in
|
||||
tqdm==4.60.0
|
||||
tqdm==4.62.2
|
||||
# via twine
|
||||
twine==3.4.1
|
||||
twine==3.4.2
|
||||
# via -r dev-requirements.in
|
||||
typed-ast==1.4.3
|
||||
# via mypy
|
||||
typing-extensions==3.10.0.0
|
||||
# via
|
||||
# libcst
|
||||
@@ -230,15 +234,17 @@ unify==0.5
|
||||
# via gray
|
||||
untokenize==0.1.1
|
||||
# via unify
|
||||
urllib3==1.26.5
|
||||
urllib3==1.26.6
|
||||
# via requests
|
||||
virtualenv==20.4.4
|
||||
virtualenv==20.7.2
|
||||
# via
|
||||
# pre-commit
|
||||
# tox
|
||||
webencodings==0.5.1
|
||||
# via bleach
|
||||
zipp==3.4.1
|
||||
wheel==0.37.0
|
||||
# via pip-tools
|
||||
zipp==3.5.0
|
||||
# via
|
||||
# importlib-metadata
|
||||
# importlib-resources
|
||||
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
FROM python:3.8-slim as aprsd
|
||||
FROM python:3-bullseye as aprsd
|
||||
|
||||
# Dockerfile for building a container during aprsd development.
|
||||
|
||||
@@ -28,7 +28,7 @@ RUN addgroup --gid $GID $APRS_USER
|
||||
RUN useradd -m -u $UID -g $APRS_USER $APRS_USER
|
||||
|
||||
# Install aprsd
|
||||
RUN /usr/local/bin/pip3 install aprsd==1.6.0
|
||||
RUN /usr/local/bin/pip3 install aprsd==2.3.0
|
||||
|
||||
# Ensure /config is there with a default config file
|
||||
USER root
|
||||
|
||||
+1
-1
@@ -36,7 +36,7 @@ do
|
||||
esac
|
||||
done
|
||||
|
||||
VERSION="1.6.0"
|
||||
VERSION="2.2.1"
|
||||
|
||||
if [ $ALL_PLATFORMS -eq 1 ]
|
||||
then
|
||||
|
||||
@@ -28,14 +28,6 @@ aprsd.dev module
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
aprsd.email module
|
||||
------------------
|
||||
|
||||
.. automodule:: aprsd.email
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
aprsd.fake\_aprs module
|
||||
-----------------------
|
||||
|
||||
@@ -68,6 +60,14 @@ aprsd.healthcheck module
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
aprsd.kissclient module
|
||||
-----------------------
|
||||
|
||||
.. automodule:: aprsd.kissclient
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
aprsd.listen module
|
||||
-------------------
|
||||
|
||||
|
||||
+4
-1
@@ -1,3 +1,4 @@
|
||||
aioax25>=0.0.10
|
||||
aprslib
|
||||
click
|
||||
click-completion
|
||||
@@ -11,10 +12,12 @@ pbr
|
||||
pyyaml
|
||||
# Allowing a newer version can lead to a conflict with
|
||||
# requests.
|
||||
py3-validate-email==0.2.16
|
||||
py3-validate-email
|
||||
pytz
|
||||
requests
|
||||
six
|
||||
thesmuggler
|
||||
yfinance
|
||||
update_checker
|
||||
flask-socketio
|
||||
eventlet
|
||||
|
||||
+55
-29
@@ -1,86 +1,107 @@
|
||||
#
|
||||
# This file is autogenerated by pip-compile
|
||||
# This file is autogenerated by pip-compile with python 3.8
|
||||
# To update, run:
|
||||
#
|
||||
# pip-compile requirements.in
|
||||
#
|
||||
aioax25==0.0.10
|
||||
# via -r requirements.in
|
||||
aprslib==0.6.47
|
||||
# via -r requirements.in
|
||||
backoff==1.10.0
|
||||
backoff==1.11.1
|
||||
# via opencage
|
||||
certifi==2020.12.5
|
||||
bidict==0.21.2
|
||||
# via python-socketio
|
||||
certifi==2021.5.30
|
||||
# via requests
|
||||
cffi==1.14.5
|
||||
cffi==1.14.6
|
||||
# via cryptography
|
||||
chardet==4.0.0
|
||||
charset-normalizer==2.0.4
|
||||
# via requests
|
||||
click-completion==0.5.2
|
||||
# via -r requirements.in
|
||||
click==7.1.2
|
||||
click==8.0.1
|
||||
# via
|
||||
# -r requirements.in
|
||||
# click-completion
|
||||
# flask
|
||||
click-completion==0.5.2
|
||||
# via -r requirements.in
|
||||
contexter==0.1.4
|
||||
# via signalslot
|
||||
cryptography==3.4.7
|
||||
# via pyopenssl
|
||||
dnspython==2.1.0
|
||||
# via py3-validate-email
|
||||
# via
|
||||
# eventlet
|
||||
# py3-validate-email
|
||||
eventlet==0.32.0
|
||||
# via -r requirements.in
|
||||
filelock==3.0.12
|
||||
# via py3-validate-email
|
||||
flask-classful==0.14.2
|
||||
# via -r requirements.in
|
||||
flask-httpauth==4.3.0
|
||||
# via -r requirements.in
|
||||
flask==1.1.2
|
||||
flask==2.0.1
|
||||
# via
|
||||
# -r requirements.in
|
||||
# flask-classful
|
||||
# flask-httpauth
|
||||
idna==2.10
|
||||
# flask-socketio
|
||||
flask-classful==0.14.2
|
||||
# via -r requirements.in
|
||||
flask-httpauth==4.4.0
|
||||
# via -r requirements.in
|
||||
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==1.1.0
|
||||
itsdangerous==2.0.1
|
||||
# via flask
|
||||
jinja2==2.11.3
|
||||
jinja2==3.0.1
|
||||
# via
|
||||
# click-completion
|
||||
# flask
|
||||
lxml==4.6.3
|
||||
# via yfinance
|
||||
markupsafe==1.1.1
|
||||
markupsafe==2.0.1
|
||||
# via jinja2
|
||||
multitasking==0.0.9
|
||||
# via yfinance
|
||||
numpy==1.20.2
|
||||
numpy==1.21.2
|
||||
# via
|
||||
# pandas
|
||||
# yfinance
|
||||
opencage==1.2.2
|
||||
opencage==2.0.0
|
||||
# via -r requirements.in
|
||||
pandas==1.2.4
|
||||
pandas==1.3.2
|
||||
# via yfinance
|
||||
pbr==5.6.0
|
||||
# via -r requirements.in
|
||||
pluggy==0.13.1
|
||||
pluggy==1.0.0
|
||||
# via -r requirements.in
|
||||
py3-validate-email==0.2.16
|
||||
py3-validate-email==1.0.1
|
||||
# via -r requirements.in
|
||||
pycparser==2.20
|
||||
# via cffi
|
||||
pyopenssl==20.0.1
|
||||
# via opencage
|
||||
pyserial==3.5
|
||||
# via aioax25
|
||||
python-dateutil==2.8.1
|
||||
# via pandas
|
||||
python-engineio==4.2.1
|
||||
# via python-socketio
|
||||
python-socketio==5.4.0
|
||||
# via flask-socketio
|
||||
pytz==2021.1
|
||||
# via
|
||||
# -r requirements.in
|
||||
# pandas
|
||||
pyyaml==5.4.1
|
||||
# via -r requirements.in
|
||||
requests==2.25.1
|
||||
requests==2.26.0
|
||||
# via
|
||||
# -r requirements.in
|
||||
# opencage
|
||||
@@ -88,21 +109,26 @@ requests==2.25.1
|
||||
# yfinance
|
||||
shellingham==1.4.0
|
||||
# via click-completion
|
||||
six==1.15.0
|
||||
signalslot==0.1.2
|
||||
# via aioax25
|
||||
six==1.16.0
|
||||
# via
|
||||
# -r requirements.in
|
||||
# click-completion
|
||||
# eventlet
|
||||
# imapclient
|
||||
# opencage
|
||||
# pyopenssl
|
||||
# python-dateutil
|
||||
# signalslot
|
||||
thesmuggler==1.0.1
|
||||
# via -r requirements.in
|
||||
update-checker==0.18.0
|
||||
# via -r requirements.in
|
||||
urllib3==1.26.5
|
||||
urllib3==1.26.6
|
||||
# via requests
|
||||
werkzeug==1.0.1
|
||||
weakrefmethod==1.0.3
|
||||
# via signalslot
|
||||
werkzeug==2.0.0
|
||||
# via flask
|
||||
yfinance==0.1.59
|
||||
yfinance==0.1.63
|
||||
# via -r requirements.in
|
||||
|
||||
@@ -30,6 +30,9 @@ def fake_packet(
|
||||
class FakeBaseNoThreadsPlugin(plugin.APRSDPluginBase):
|
||||
version = "1.0"
|
||||
|
||||
def setup(self):
|
||||
self.enabled = True
|
||||
|
||||
def filter(self, packet):
|
||||
return None
|
||||
|
||||
@@ -48,6 +51,9 @@ class FakeThread(threads.APRSDThread):
|
||||
class FakeBaseThreadsPlugin(plugin.APRSDPluginBase):
|
||||
version = "1.0"
|
||||
|
||||
def setup(self):
|
||||
self.enabled = True
|
||||
|
||||
def filter(self, packet):
|
||||
return None
|
||||
|
||||
|
||||
Reference in New Issue
Block a user