1
0
mirror of https://github.com/craigerl/aprsd.git synced 2026-08-16 00:23:34 -04:00

Compare commits

...

31 Commits

Author SHA1 Message Date
hemna 560e152742 Fixed issue of aprs-is missing keepalive
Started noticing that aprs-is keepalive messages just stop
getting sent.  This causes aprsd to basically disconnect from
the APRS network.  Added a check into the KeepAlive thread to
restart the aprs-is connecter if the last time we got a keepalive
from apris is > 5 minutes.
2021-09-13 13:22:06 -04:00
hemna 69b215d4d8 Fixed packet processing issue with aprsd send-message
This patch adds the missing PacketList initialization
for the send-message command
2021-09-10 15:39:07 -04:00
hemna 4164e89016 Prep 2.3.0 2021-09-08 14:54:59 -04:00
hemna 1b9a9935fc Enable plugins to return message object
This patch enables the ability for plugins to return:
* string
* list of strings
* message object
* list of strings and message ojects

Each string will be encapsulated in a message object prior being sent.
each message object will be sent directly.
Each list will be iterated over and processed according to the above 2
options.
2021-09-08 14:45:15 -04:00
hemna 3faf41b203 Added enabled flag for every plugin object
This allows the admin interface to see which plugins are registered and
enabled.  Enabled is a flag that is set in the setup() method of the
plugin.  This gives the plugin developer a chance to disable the plugin
if something isn't right at setup time.   This allows aprsd to ignore
plugins that are registered but not emabled.
2021-09-08 14:25:12 -04:00
hemna 7e6dffb34b Ensure plugin threads are valid
This patch makes sure that the plugin threads returned from
create_threads is somewhat valid
2021-09-08 13:44:20 -04:00
hemna 605911cb84 Updated Dockerfile to use v2.3.0
Upcoming release of aprsd is v2.3.0
2021-09-07 13:56:06 -04:00
hemna 9eff99dde7 Removed fixed size on logging queue
If the logging queue gets full, due to a maxsize being set,
then any further logs will result on lots of errors being dumped
to stderr as the queue is full.
2021-09-07 13:43:48 -04:00
hemna d6b3df93f1 Added Logfile tab in Admin ui
This patch adds a live view of the aprsd logfile in
the admin ui.  This uses a new Log QueueHandler and the
threads.logging_queue to push log entries into a queue.
The flask websockets server will push those log entries up
to a connected client browser.
2021-09-07 13:13:36 -04:00
hemna 4f088e0a4a Updated Makefile clean target
This expands the clean target to clean up more stuff.
2021-09-05 19:47:26 -04:00
hemna d643ca3892 Added self creating Makefile help target
This updates the Makefile to include a default target of help
and it automatically generates the help based on the targets
and their descriptions.
2021-09-03 16:49:16 -04:00
hemna dfaf3aa3d1 Update dev.py
This patch ensures a valid packet is passed into the plugin prior to
testing.
2021-09-03 16:48:00 -04:00
hemna 62ce84b315 Allow passing in aprsis_client
When the admin user users the web ui to send a message
a new client instance is created with login credentials for
that particular message.  This patch ensures that send_direct
uses that client.
2021-09-02 11:17:15 -04:00
hemna 8ada789d4d Fixed a problem with the AVWX plugin not working
the regex for the plugin was not matching correctly
2021-09-02 11:06:25 -04:00
hemna 558710d348 Remove some noisy trace in email plugin
This removes the trace decorators from the email login
functions.  They have been stable for a while now.
2021-09-02 10:03:43 -04:00
hemna 1ea6c05dec Fixed issue at startup with notify plugin
Ensure that the aprsis client is configured prior to starting
any plugins.
2021-09-02 09:54:13 -04:00
hemna 0f6df5fc05 Fixed email validation
This patch adjusts the py3-email-validation usage.  Since we
upgraded to 1.0.2, the signature has changed.  This patch adjusts
the signature usage so it works again.
2021-09-02 09:43:33 -04:00
hemna 1635feb820 Removed values from forms 2021-09-02 09:12:44 -04:00
hemna c58031d772 Added send-message to the main admin UI 2021-09-02 08:56:25 -04:00
hemna 266ae7f217 Merge pull request #59 from craigerl/web-send-msg
Send Message via admin Web interface
2021-09-01 17:44:50 -04:00
hemna c537b54df6 Updated requirements 2021-09-01 17:38:59 -04:00
hemna 84ce60bc50 Cleaned up some pep8 failures 2021-09-01 17:11:35 -04:00
hemna c941379a5c Upgraded the send-message POC to use websockets
This patch updates the send message Admi page to use
websockets.  It makes updates to the messages list instant.
2021-09-01 17:10:59 -04:00
hemna 23cbf32814 New Admin ui send message page working. 2021-09-01 17:10:13 -04:00
hemna 6d3258e833 Send Message via admin Web interface
This patch adds the ability to send a message from the
admin interface's send-message.html page.
2021-09-01 17:06:56 -04:00
hemna d243e577f0 Merge pull request #50 from craigerl/tcpkiss
Added the ability to use direwolf KISS socket
2021-09-01 16:58:57 -04:00
hemna ca438c9c60 Updated Admin UI to show KISS connections
This updates the top area of the Admin UI to reflect the
connection type (aprs-is vs kiss).
2021-09-01 16:39:50 -04:00
hemna f4dee4b202 Got TX/RX working with aioax25+direwolf over TCP
This patch gets APRSD fully working with the TCPKISS socket
to direwolf.
2021-09-01 14:48:22 -04:00
hemna 54c9a6b55a Rebased from master 2021-08-30 13:34:25 -04:00
hemna b53e2ba7fe Added the ability to use direwolf KISS socket
This patch adds APRS KISS connectivity.  I have tested this with
a running Direwolf install via either a serial KISS connection or
the optional new TCPKISS connection, both to Direwolf.

This adds the new required aioax25 python library for the underlying
KISS and AX25 support.

NOTE: For the TCPKISS connection, this patch requires a pull request
patch the aioax25 library to include a TCP Based KISS TNC client to
enable the TCPKISS client  So you will need to pull down this PR
https://github.com/sjlongland/aioax25/pull/7

To enable this,
  Edit your aprsd.yml file and enable one of the 2 KISS connections.
  Only one is supported at a time.

  kiss:
     serial:
         enabled: True
         device: /dev/ttyS1
         baudrate: 9600

  or

  kiss:
      tcp:
          enabled: True
          host: "ip address/hostname of direwolf"
          port: "direwolf configured kiss port"

This patch alters the Message object classes to be able to
send messages out via the aprslib socket connection to the APRS-IS
network on the internet, or via the direwolf KISS TCP socket,
depending on the origination of the initial message coming in.

If an APRS message comes in via APRS-IS, then replies will go out
APRS-IS.  IF an APRS message comes in via direwolf, then replies
will go out via direwolf KISS TCP socket.   Both can work at the same
time.

TODO:  I need some real APRS message packets to verify that
the new thread is processing packets correctly through the plugins
and able to send the resulting messages back out to direwolf.

Have a hard coded callsign for now in the kissclient consumer call,
just so I can see messages coming in from direwolf.  I dont' have an
APRS capable radio at the moment to send messages directly to direwolf.
Might need to write a simple python socket server to send fake APRS
messages to aprsd kiss, just for finishing up development.
2021-08-30 13:28:39 -04:00
hemna a7d79a6e1b Update Dockerfile to use 2.2.1 2021-08-26 10:50:34 -04:00
31 changed files with 3808 additions and 225 deletions
+31
View File
@@ -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
+43 -28
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
+152
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
+6 -8
View File
@@ -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(
+2 -2
View File
@@ -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)
+1
View File
@@ -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
View File
@@ -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
View File
@@ -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,
+189
View File
@@ -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;
}
+1 -1
View File
@@ -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);
+26
View File
@@ -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;
}
+5 -2
View File
@@ -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
+144
View File
@@ -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 + "&nbsp;&nbsp;&nbsp;&nbsp;" + from + " > "
if (msg.hasOwnProperty('to')) {
from_to = from_to + msg['to']
}
from_to = from_to + "&nbsp;&nbsp;-&nbsp;&nbsp;" + 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>&nbsp;';
msg_html += '<i class="thumbs down outline icon" id="' + ack_id + '" data-content="Waiting for ACK"></i>&nbsp;';
msg_html += '<i class="thumbs down outline icon" id="' + reply_id + '" data-content="Waiting for Reply"></i>&nbsp;';
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 + "&nbsp;&nbsp;&nbsp;&nbsp;" + from + " > "
if (msg.hasOwnProperty('to')) {
from_to = from_to + msg['to']
}
from_to = from_to + "&nbsp;&nbsp;-&nbsp;&nbsp;" + msg['message']
from_to += "&nbsp;&nbsp; ===> " + 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');
}
+44 -5
View File
@@ -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>
+74
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -36,7 +36,7 @@ do
esac
done
VERSION="1.6.0"
VERSION="2.2.1"
if [ $ALL_PLATFORMS -eq 1 ]
then
+8 -8
View File
@@ -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
View File
@@ -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
View File
@@ -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
+6
View File
@@ -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