mirror of
https://github.com/craigerl/aprsd.git
synced 2026-08-16 16:43:52 -04:00
Compare commits
58 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| bda2ef00dd | |||
| 446484e631 | |||
| a8a6b1aa07 | |||
| 8842fb1b44 | |||
| 152132b0ed | |||
| 7787dc1be4 | |||
| 10e34d8634 | |||
| 9469410929 | |||
| 998bc32c27 | |||
| 88db485eb4 | |||
| 5d17809895 | |||
| 059cc86a11 | |||
| ffdd1e47b2 | |||
| cdcb98e438 | |||
| 89727e2b8e | |||
| 617973f561 | |||
| 9187b9781a | |||
| 8287c09ce5 | |||
| 82def598f0 | |||
| 3463c6eb96 | |||
| 2ead6a97da | |||
| 7d0006b0a6 | |||
| 30df452e00 | |||
| 49f3ea8339 | |||
| 0d5b7166b3 | |||
| cefb581bb8 | |||
| d2e8fe660f | |||
| 95fecd2394 | |||
| c8c23e6185 | |||
| a3a3a5aa23 | |||
| e009791b75 | |||
| b0d25a76f7 | |||
| 89701c8a70 | |||
| 66c5d85b89 | |||
| 8ee8b149f1 | |||
| 0d51634ec2 | |||
| 135e21cd8d | |||
| 4233827dea | |||
| 9b2212245f | |||
| 9150f3b6ff | |||
| 278bb6e882 | |||
| 004795dbf1 | |||
| 3b7924b13d | |||
| 2bf85db21b | |||
| 14f77876f9 | |||
| db9cbf51df | |||
| 5b17228811 | |||
| 725bb2fe35 | |||
| f8d87d05bb | |||
| 30671cbdbc | |||
| fdc8c0cd66 | |||
| c097c31258 | |||
| e3c5c7b408 | |||
| 491644ece6 | |||
| a6ed7b894b | |||
| 270be947b5 | |||
| 23e3876e7b | |||
| 65ea33290a |
@@ -1,9 +1,94 @@
|
||||
CHANGES
|
||||
=======
|
||||
|
||||
v2.5.2
|
||||
------
|
||||
|
||||
* Added new list-plugins command
|
||||
* Don't require check-version command to have a config
|
||||
* Healthcheck command doesn't need the aprsd.yml config
|
||||
* Fix test failures
|
||||
* Removed requirement for aprs.fi key
|
||||
* Updated Changelog
|
||||
|
||||
v2.5.1
|
||||
------
|
||||
|
||||
* Removed stock plugin
|
||||
* Removed the stock plugin
|
||||
|
||||
v2.5.0
|
||||
------
|
||||
|
||||
* Updated for v2.5.0
|
||||
* Updated Dockerfile's and build script for docker
|
||||
* Cleaned up some verbose output & colorized output
|
||||
* Reworked all the common arguments
|
||||
* Fixed test-plugin
|
||||
* Ensure common params are honored
|
||||
* pep8
|
||||
* Added healthcheck to the cmds
|
||||
* Removed the need for FROMCALL in dev test-plugin
|
||||
* Pep8 failures
|
||||
* Refactor the cli
|
||||
* Updated Changelog for 4.2.3
|
||||
* Fixed a problem with send-message command
|
||||
|
||||
v2.4.2
|
||||
------
|
||||
|
||||
* Updated Changelog
|
||||
* Be more careful picking data to/from disk
|
||||
* Updated Changelog
|
||||
|
||||
v2.4.1
|
||||
------
|
||||
|
||||
* Ensure plugins are last to be loaded
|
||||
* Fixed email connecting to smtp server
|
||||
|
||||
v2.4.0
|
||||
------
|
||||
|
||||
* Updated Changelog for 2.4.0 release
|
||||
* Converted MsgTrack to ObjectStoreMixin
|
||||
* Fixed unit tests
|
||||
* Make sure SeenList update has a from in packet
|
||||
* Ensure PacketList is initialized
|
||||
* Added SIGTERM to signal\_handler
|
||||
* Enable configuring where to save the objectstore data
|
||||
* PEP8 cleanup
|
||||
* Added objectstore Mixin
|
||||
* Added -num option to aprsd-dev test-plugin
|
||||
* Only call stop\_threads if it exists
|
||||
* Added new SeenList
|
||||
* Added plugin version to stats reporting
|
||||
* Added new HelpPlugin
|
||||
* Updated aprsd-dev to use config for logfile format
|
||||
* Updated build.sh
|
||||
* removed usage of config.check\_config\_option
|
||||
* Fixed send-message after config/client rework
|
||||
* Fixed issue with flask config
|
||||
* Added some server startup info logs
|
||||
* Increase email delay to +10
|
||||
* Updated dev to use plugin manager
|
||||
* Fixed notify plugins
|
||||
* Added new Config object
|
||||
* Fixed email plugin's use of globals
|
||||
* Refactored client classes
|
||||
* Refactor utils usage
|
||||
* 2.3.1 Changelog
|
||||
|
||||
v2.3.1
|
||||
------
|
||||
|
||||
* Fixed issue of aprs-is missing keepalive
|
||||
* Fixed packet processing issue with aprsd send-message
|
||||
|
||||
v2.3.0
|
||||
------
|
||||
|
||||
* Prep 2.3.0
|
||||
* Enable plugins to return message object
|
||||
* Added enabled flag for every plugin object
|
||||
* Ensure plugin threads are valid
|
||||
|
||||
+127
@@ -0,0 +1,127 @@
|
||||
#
|
||||
# Listen on amateur radio aprs-is network for messages and respond to them.
|
||||
# You must have an amateur radio callsign to use this software. You must
|
||||
# create an ~/.aprsd/config.yml file with all of the required settings. To
|
||||
# generate an example config.yml, just run aprsd, then copy the sample config
|
||||
# to ~/.aprsd/config.yml and edit the settings.
|
||||
#
|
||||
# APRS messages:
|
||||
# l(ocation) = descriptive location of calling station
|
||||
# w(eather) = temp, (hi/low) forecast, later forecast
|
||||
# t(ime) = respond with the current time
|
||||
# f(ortune) = respond with a short fortune
|
||||
# -email_addr email text = send an email
|
||||
# -2 = display the last 2 emails received
|
||||
# p(ing) = respond with Pong!/time
|
||||
# anything else = respond with usage
|
||||
#
|
||||
# (C)2018 Craig Lamparter
|
||||
# License GPLv2
|
||||
#
|
||||
|
||||
# python included libs
|
||||
import datetime
|
||||
import logging
|
||||
import os
|
||||
import signal
|
||||
import sys
|
||||
import time
|
||||
|
||||
import click
|
||||
import click_completion
|
||||
|
||||
# local imports here
|
||||
import aprsd
|
||||
from aprsd import cli_helper
|
||||
from aprsd import config as aprsd_config
|
||||
from aprsd import messaging, packets, stats, threads, utils
|
||||
|
||||
|
||||
# setup the global logger
|
||||
# logging.basicConfig(level=logging.DEBUG) # level=10
|
||||
LOG = logging.getLogger("APRSD")
|
||||
CONTEXT_SETTINGS = dict(help_option_names=["-h", "--help"])
|
||||
flask_enabled = False
|
||||
|
||||
|
||||
def custom_startswith(string, incomplete):
|
||||
"""A custom completion match that supports case insensitive matching."""
|
||||
if os.environ.get("_CLICK_COMPLETION_COMMAND_CASE_INSENSITIVE_COMPLETE"):
|
||||
string = string.lower()
|
||||
incomplete = incomplete.lower()
|
||||
return string.startswith(incomplete)
|
||||
|
||||
|
||||
click_completion.core.startswith = custom_startswith
|
||||
click_completion.init()
|
||||
|
||||
|
||||
@click.group(context_settings=CONTEXT_SETTINGS)
|
||||
@click.version_option()
|
||||
@click.pass_context
|
||||
def cli(ctx):
|
||||
pass
|
||||
|
||||
|
||||
def main():
|
||||
# First import all the possible commands for the CLI
|
||||
# The commands themselves live in the cmds directory
|
||||
from .cmds import ( # noqa
|
||||
completion, dev, healthcheck, list_plugins, listen, send_message,
|
||||
server,
|
||||
)
|
||||
cli()
|
||||
|
||||
|
||||
def signal_handler(sig, frame):
|
||||
global flask_enabled
|
||||
|
||||
click.echo("signal_handler: called")
|
||||
threads.APRSDThreadList().stop_all()
|
||||
if "subprocess" not in str(frame):
|
||||
LOG.info(
|
||||
"Ctrl+C, Sending all threads exit! Can take up to 10 seconds {}".format(
|
||||
datetime.datetime.now(),
|
||||
),
|
||||
)
|
||||
time.sleep(1.5)
|
||||
messaging.MsgTrack().save()
|
||||
packets.WatchList().save()
|
||||
packets.SeenList().save()
|
||||
LOG.info(stats.APRSDStats())
|
||||
# signal.signal(signal.SIGTERM, sys.exit(0))
|
||||
# sys.exit(0)
|
||||
if flask_enabled:
|
||||
signal.signal(signal.SIGTERM, sys.exit(0))
|
||||
|
||||
|
||||
@cli.command()
|
||||
@cli_helper.add_options(cli_helper.common_options)
|
||||
@click.pass_context
|
||||
@cli_helper.process_standard_options_no_config
|
||||
def check_version(ctx):
|
||||
"""Check this version against the latest in pypi.org."""
|
||||
level, msg = utils._check_version()
|
||||
if level:
|
||||
click.secho(msg, fg="yellow")
|
||||
else:
|
||||
click.secho(msg, fg="green")
|
||||
|
||||
|
||||
@cli.command()
|
||||
@click.pass_context
|
||||
def sample_config(ctx):
|
||||
"""This dumps the config to stdout."""
|
||||
click.echo(aprsd_config.dump_default_cfg())
|
||||
|
||||
|
||||
@cli.command()
|
||||
@click.pass_context
|
||||
def version(ctx):
|
||||
"""Show the APRSD version."""
|
||||
click.echo(click.style("APRSD Version : ", fg="white"), nl=False)
|
||||
click.secho(f"{aprsd.__version__}", fg="yellow", bold=True)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,88 @@
|
||||
from functools import update_wrapper
|
||||
import typing as t
|
||||
|
||||
import click
|
||||
|
||||
from aprsd import config as aprsd_config
|
||||
from aprsd import log
|
||||
|
||||
|
||||
F = t.TypeVar("F", bound=t.Callable[..., t.Any])
|
||||
|
||||
common_options = [
|
||||
click.option(
|
||||
"--loglevel",
|
||||
default="INFO",
|
||||
show_default=True,
|
||||
type=click.Choice(
|
||||
["CRITICAL", "ERROR", "WARNING", "INFO", "DEBUG"],
|
||||
case_sensitive=False,
|
||||
),
|
||||
show_choices=True,
|
||||
help="The log level to use for aprsd.log",
|
||||
),
|
||||
click.option(
|
||||
"-c",
|
||||
"--config",
|
||||
"config_file",
|
||||
show_default=True,
|
||||
default=aprsd_config.DEFAULT_CONFIG_FILE,
|
||||
help="The aprsd config file to use for options.",
|
||||
),
|
||||
click.option(
|
||||
"--quiet",
|
||||
is_flag=True,
|
||||
default=False,
|
||||
help="Don't log to stdout",
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
def add_options(options):
|
||||
def _add_options(func):
|
||||
for option in reversed(options):
|
||||
func = option(func)
|
||||
return func
|
||||
return _add_options
|
||||
|
||||
|
||||
def process_standard_options(f: F) -> F:
|
||||
def new_func(*args, **kwargs):
|
||||
ctx = args[0]
|
||||
ctx.ensure_object(dict)
|
||||
ctx.obj["loglevel"] = kwargs["loglevel"]
|
||||
ctx.obj["config_file"] = kwargs["config_file"]
|
||||
ctx.obj["quiet"] = kwargs["quiet"]
|
||||
ctx.obj["config"] = aprsd_config.parse_config(kwargs["config_file"])
|
||||
log.setup_logging(
|
||||
ctx.obj["config"], ctx.obj["loglevel"],
|
||||
ctx.obj["quiet"],
|
||||
)
|
||||
|
||||
del kwargs["loglevel"]
|
||||
del kwargs["config_file"]
|
||||
del kwargs["quiet"]
|
||||
return f(*args, **kwargs)
|
||||
|
||||
return update_wrapper(t.cast(F, new_func), f)
|
||||
|
||||
|
||||
def process_standard_options_no_config(f: F) -> F:
|
||||
"""Use this as a decorator when config isn't needed."""
|
||||
def new_func(*args, **kwargs):
|
||||
ctx = args[0]
|
||||
ctx.ensure_object(dict)
|
||||
ctx.obj["loglevel"] = kwargs["loglevel"]
|
||||
ctx.obj["config_file"] = kwargs["config_file"]
|
||||
ctx.obj["quiet"] = kwargs["quiet"]
|
||||
log.setup_logging_no_config(
|
||||
ctx.obj["loglevel"],
|
||||
ctx.obj["quiet"],
|
||||
)
|
||||
|
||||
del kwargs["loglevel"]
|
||||
del kwargs["config_file"]
|
||||
del kwargs["quiet"]
|
||||
return f(*args, **kwargs)
|
||||
|
||||
return update_wrapper(t.cast(F, new_func), f)
|
||||
+133
-206
@@ -1,26 +1,30 @@
|
||||
import abc
|
||||
import logging
|
||||
import select
|
||||
import time
|
||||
|
||||
import aprslib
|
||||
from aprslib import is_py3
|
||||
from aprslib.exceptions import (
|
||||
ConnectionDrop, ConnectionError, GenericError, LoginError, ParseError,
|
||||
UnknownFormat,
|
||||
)
|
||||
from aprslib.exceptions import LoginError
|
||||
|
||||
import aprsd
|
||||
from aprsd import stats
|
||||
from aprsd import trace
|
||||
from aprsd.clients import aprsis, kiss
|
||||
|
||||
|
||||
LOG = logging.getLogger("APRSD")
|
||||
TRANSPORT_APRSIS = "aprsis"
|
||||
TRANSPORT_TCPKISS = "tcpkiss"
|
||||
TRANSPORT_SERIALKISS = "serialkiss"
|
||||
|
||||
# Main must create this from the ClientFactory
|
||||
# object such that it's populated with the
|
||||
# Correct config
|
||||
factory = None
|
||||
|
||||
|
||||
class Client:
|
||||
"""Singleton client class that constructs the aprslib connection."""
|
||||
|
||||
_instance = None
|
||||
aprs_client = None
|
||||
_client = None
|
||||
config = None
|
||||
|
||||
connected = False
|
||||
@@ -38,21 +42,51 @@ 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:
|
||||
self.aprs_client = self.setup_connection()
|
||||
return self.aprs_client
|
||||
if not self._client:
|
||||
self._client = self.setup_connection()
|
||||
return self._client
|
||||
|
||||
def reset(self):
|
||||
"""Call this to force a rebuild/reconnect."""
|
||||
del self.aprs_client
|
||||
del self._client
|
||||
|
||||
@abc.abstractmethod
|
||||
def setup_connection(self):
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
@abc.abstractmethod
|
||||
def is_enabled(config):
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
@abc.abstractmethod
|
||||
def transport(config):
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def decode_packet(self, *args, **kwargs):
|
||||
pass
|
||||
|
||||
|
||||
class APRSISClient(Client):
|
||||
|
||||
@staticmethod
|
||||
def is_enabled(config):
|
||||
# Defaults to True if the enabled flag is non existent
|
||||
return config["aprs"].get("enabled", True)
|
||||
|
||||
@staticmethod
|
||||
def transport(config):
|
||||
return TRANSPORT_APRSIS
|
||||
|
||||
def decode_packet(self, *args, **kwargs):
|
||||
"""APRS lib already decodes this."""
|
||||
return args[0]
|
||||
|
||||
@trace.trace
|
||||
def setup_connection(self):
|
||||
user = self.config["aprs"]["login"]
|
||||
password = self.config["aprs"]["password"]
|
||||
@@ -60,10 +94,11 @@ class Client:
|
||||
port = self.config["aprs"].get("port", 14580)
|
||||
connected = False
|
||||
backoff = 1
|
||||
aprs_client = None
|
||||
while not connected:
|
||||
try:
|
||||
LOG.info("Creating aprslib client")
|
||||
aprs_client = Aprsdis(user, passwd=password, host=host, port=port)
|
||||
aprs_client = aprsis.Aprsdis(user, passwd=password, host=host, port=port)
|
||||
# Force the logging to be the same
|
||||
aprs_client.logger = LOG
|
||||
aprs_client.connect()
|
||||
@@ -82,200 +117,92 @@ class Client:
|
||||
return aprs_client
|
||||
|
||||
|
||||
class Aprsdis(aprslib.IS):
|
||||
"""Extend the aprslib class so we can exit properly."""
|
||||
class KISSClient(Client):
|
||||
|
||||
# flag to tell us to stop
|
||||
thread_stop = False
|
||||
@staticmethod
|
||||
def is_enabled(config):
|
||||
"""Return if tcp or serial KISS is enabled."""
|
||||
if "kiss" not in config:
|
||||
return False
|
||||
|
||||
# timeout in seconds
|
||||
select_timeout = 1
|
||||
if config.get("kiss.serial.enabled", default=False):
|
||||
return True
|
||||
|
||||
def stop(self):
|
||||
self.thread_stop = True
|
||||
LOG.info("Shutdown Aprsdis client.")
|
||||
if config.get("kiss.tcp.enabled", default=False):
|
||||
return True
|
||||
|
||||
def send(self, msg):
|
||||
"""Send an APRS Message object."""
|
||||
line = str(msg)
|
||||
self.sendall(line)
|
||||
@staticmethod
|
||||
def transport(config):
|
||||
if config.get("kiss.serial.enabled", default=False):
|
||||
return TRANSPORT_SERIALKISS
|
||||
|
||||
def _socket_readlines(self, blocking=False):
|
||||
"""
|
||||
Generator for complete lines, received from the server
|
||||
"""
|
||||
try:
|
||||
self.sock.setblocking(0)
|
||||
except OSError as e:
|
||||
self.logger.error(f"socket error when setblocking(0): {str(e)}")
|
||||
raise aprslib.ConnectionDrop("connection dropped")
|
||||
if config.get("kiss.tcp.enabled", default=False):
|
||||
return TRANSPORT_TCPKISS
|
||||
|
||||
while not self.thread_stop:
|
||||
short_buf = b""
|
||||
newline = b"\r\n"
|
||||
def decode_packet(self, *args, **kwargs):
|
||||
"""We get a frame, which has to be decoded."""
|
||||
frame = kwargs["frame"]
|
||||
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}")
|
||||
|
||||
# set a select timeout, so we get a chance to exit
|
||||
# when user hits CTRL-C
|
||||
readable, writable, exceptional = select.select(
|
||||
[self.sock],
|
||||
[],
|
||||
[],
|
||||
self.select_timeout,
|
||||
)
|
||||
if not readable:
|
||||
if not blocking:
|
||||
break
|
||||
else:
|
||||
continue
|
||||
packet = aprslib.parse(msg)
|
||||
return packet
|
||||
|
||||
try:
|
||||
short_buf = self.sock.recv(4096)
|
||||
|
||||
# sock.recv returns empty if the connection drops
|
||||
if not short_buf:
|
||||
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):
|
||||
if not blocking:
|
||||
if len(self.buf) == 0:
|
||||
break
|
||||
|
||||
self.buf += short_buf
|
||||
|
||||
while newline in self.buf:
|
||||
line, self.buf = self.buf.split(newline, 1)
|
||||
|
||||
yield line
|
||||
|
||||
def _send_login(self):
|
||||
"""
|
||||
Sends login string to server
|
||||
"""
|
||||
login_str = "user {0} pass {1} vers github.com/craigerl/aprsd {3}{2}\r\n"
|
||||
login_str = login_str.format(
|
||||
self.callsign,
|
||||
self.passwd,
|
||||
(" filter " + self.filter) if self.filter != "" else "",
|
||||
aprsd.__version__,
|
||||
)
|
||||
|
||||
self.logger.info("Sending login information")
|
||||
|
||||
try:
|
||||
self._sendall(login_str)
|
||||
self.sock.settimeout(5)
|
||||
test = self.sock.recv(len(login_str) + 100)
|
||||
if is_py3:
|
||||
test = test.decode("latin-1")
|
||||
test = test.rstrip()
|
||||
|
||||
self.logger.debug("Server: %s", test)
|
||||
|
||||
a, b, callsign, status, e = test.split(" ", 4)
|
||||
s = e.split(",")
|
||||
if len(s):
|
||||
server_string = s[0].replace("server ", "")
|
||||
else:
|
||||
server_string = e.replace("server ", "")
|
||||
|
||||
self.logger.info(f"Connected to {server_string}")
|
||||
self.server_string = server_string
|
||||
stats.APRSDStats().set_aprsis_server(server_string)
|
||||
|
||||
if callsign == "":
|
||||
raise LoginError("Server responded with empty callsign???")
|
||||
if callsign != self.callsign:
|
||||
raise LoginError(f"Server: {test}")
|
||||
if status != "verified," and self.passwd != "-1":
|
||||
raise LoginError("Password is incorrect")
|
||||
|
||||
if self.passwd == "-1":
|
||||
self.logger.info("Login successful (receive only)")
|
||||
else:
|
||||
self.logger.info("Login successful")
|
||||
|
||||
except LoginError as e:
|
||||
self.logger.error(str(e))
|
||||
self.close()
|
||||
raise
|
||||
except Exception as e:
|
||||
self.close()
|
||||
self.logger.error(f"Failed to login '{e}'")
|
||||
raise LoginError("Failed to login")
|
||||
|
||||
def consumer(self, callback, blocking=True, immortal=False, raw=False):
|
||||
"""
|
||||
When a position sentence is received, it will be passed to the callback function
|
||||
|
||||
blocking: if true (default), runs forever, otherwise will return after one sentence
|
||||
You can still exit the loop, by raising StopIteration in the callback function
|
||||
|
||||
immortal: When true, consumer will try to reconnect and stop propagation of Parse exceptions
|
||||
if false (default), consumer will return
|
||||
|
||||
raw: when true, raw packet is passed to callback, otherwise the result from aprs.parse()
|
||||
"""
|
||||
|
||||
if not self._connected:
|
||||
raise ConnectionError("not connected to a server")
|
||||
|
||||
line = b""
|
||||
|
||||
while True and not self.thread_stop:
|
||||
try:
|
||||
for line in self._socket_readlines(blocking):
|
||||
if line[0:1] != b"#":
|
||||
if raw:
|
||||
callback(line)
|
||||
else:
|
||||
callback(self._parse(line))
|
||||
else:
|
||||
self.logger.debug("Server: %s", line.decode("utf8"))
|
||||
stats.APRSDStats().set_aprsis_keepalive()
|
||||
except ParseError as exp:
|
||||
self.logger.log(
|
||||
11,
|
||||
"%s\n Packet: %s",
|
||||
exp,
|
||||
exp.packet,
|
||||
)
|
||||
except UnknownFormat as exp:
|
||||
self.logger.log(
|
||||
9,
|
||||
"%s\n Packet: %s",
|
||||
exp,
|
||||
exp.packet,
|
||||
)
|
||||
except LoginError as exp:
|
||||
self.logger.error("%s: %s", exp.__class__.__name__, exp)
|
||||
except (KeyboardInterrupt, SystemExit):
|
||||
raise
|
||||
except (ConnectionDrop, ConnectionError):
|
||||
self.close()
|
||||
|
||||
if not immortal:
|
||||
raise
|
||||
else:
|
||||
self.connect(blocking=blocking)
|
||||
continue
|
||||
except GenericError:
|
||||
pass
|
||||
except StopIteration:
|
||||
break
|
||||
except Exception:
|
||||
self.logger.error("APRS Packet: %s", line)
|
||||
raise
|
||||
|
||||
if not blocking:
|
||||
break
|
||||
@trace.trace
|
||||
def setup_connection(self):
|
||||
ax25client = kiss.Aioax25Client(self.config)
|
||||
return ax25client
|
||||
|
||||
|
||||
def get_client():
|
||||
cl = Client()
|
||||
return cl.client
|
||||
class ClientFactory:
|
||||
_instance = None
|
||||
|
||||
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.
|
||||
return cls._instance
|
||||
|
||||
def __init__(self, config):
|
||||
self.config = config
|
||||
self._builders = {}
|
||||
|
||||
def register(self, key, builder):
|
||||
self._builders[key] = builder
|
||||
|
||||
def create(self, key=None):
|
||||
if not key:
|
||||
if APRSISClient.is_enabled(self.config):
|
||||
key = TRANSPORT_APRSIS
|
||||
elif KISSClient.is_enabled(self.config):
|
||||
key = KISSClient.transport(self.config)
|
||||
|
||||
LOG.debug(f"GET client {key}")
|
||||
builder = self._builders.get(key)
|
||||
if not builder:
|
||||
raise ValueError(key)
|
||||
return builder(self.config)
|
||||
|
||||
def is_client_enabled(self):
|
||||
"""Make sure at least one client is enabled."""
|
||||
enabled = False
|
||||
for key in self._builders.keys():
|
||||
enabled |= self._builders[key].is_enabled(self.config)
|
||||
|
||||
return enabled
|
||||
|
||||
@staticmethod
|
||||
def setup(config):
|
||||
"""Create and register all possible client objects."""
|
||||
global factory
|
||||
|
||||
factory = ClientFactory(config)
|
||||
factory.register(TRANSPORT_APRSIS, APRSISClient)
|
||||
factory.register(TRANSPORT_TCPKISS, KISSClient)
|
||||
factory.register(TRANSPORT_SERIALKISS, KISSClient)
|
||||
|
||||
@@ -0,0 +1,209 @@
|
||||
import logging
|
||||
import select
|
||||
|
||||
import aprslib
|
||||
from aprslib import is_py3
|
||||
from aprslib.exceptions import (
|
||||
ConnectionDrop, ConnectionError, GenericError, LoginError, ParseError,
|
||||
UnknownFormat,
|
||||
)
|
||||
|
||||
import aprsd
|
||||
from aprsd import stats
|
||||
|
||||
|
||||
LOG = logging.getLogger("APRSD")
|
||||
|
||||
|
||||
class Aprsdis(aprslib.IS):
|
||||
"""Extend the aprslib class so we can exit properly."""
|
||||
|
||||
# flag to tell us to stop
|
||||
thread_stop = False
|
||||
|
||||
# timeout in seconds
|
||||
select_timeout = 1
|
||||
|
||||
def stop(self):
|
||||
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
|
||||
"""
|
||||
try:
|
||||
self.sock.setblocking(0)
|
||||
except OSError as e:
|
||||
self.logger.error(f"socket error when setblocking(0): {str(e)}")
|
||||
raise aprslib.ConnectionDrop("connection dropped")
|
||||
|
||||
while not self.thread_stop:
|
||||
short_buf = b""
|
||||
newline = b"\r\n"
|
||||
|
||||
# set a select timeout, so we get a chance to exit
|
||||
# when user hits CTRL-C
|
||||
readable, writable, exceptional = select.select(
|
||||
[self.sock],
|
||||
[],
|
||||
[],
|
||||
self.select_timeout,
|
||||
)
|
||||
if not readable:
|
||||
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:
|
||||
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):
|
||||
if not blocking:
|
||||
if len(self.buf) == 0:
|
||||
break
|
||||
|
||||
self.buf += short_buf
|
||||
|
||||
while newline in self.buf:
|
||||
line, self.buf = self.buf.split(newline, 1)
|
||||
|
||||
yield line
|
||||
|
||||
def _send_login(self):
|
||||
"""
|
||||
Sends login string to server
|
||||
"""
|
||||
login_str = "user {0} pass {1} vers github.com/craigerl/aprsd {3}{2}\r\n"
|
||||
login_str = login_str.format(
|
||||
self.callsign,
|
||||
self.passwd,
|
||||
(" filter " + self.filter) if self.filter != "" else "",
|
||||
aprsd.__version__,
|
||||
)
|
||||
|
||||
self.logger.info("Sending login information")
|
||||
|
||||
try:
|
||||
self._sendall(login_str)
|
||||
self.sock.settimeout(5)
|
||||
test = self.sock.recv(len(login_str) + 100)
|
||||
if is_py3:
|
||||
test = test.decode("latin-1")
|
||||
test = test.rstrip()
|
||||
|
||||
self.logger.debug("Server: %s", test)
|
||||
|
||||
a, b, callsign, status, e = test.split(" ", 4)
|
||||
s = e.split(",")
|
||||
if len(s):
|
||||
server_string = s[0].replace("server ", "")
|
||||
else:
|
||||
server_string = e.replace("server ", "")
|
||||
|
||||
self.logger.info(f"Connected to {server_string}")
|
||||
self.server_string = server_string
|
||||
stats.APRSDStats().set_aprsis_server(server_string)
|
||||
|
||||
if callsign == "":
|
||||
raise LoginError("Server responded with empty callsign???")
|
||||
if callsign != self.callsign:
|
||||
raise LoginError(f"Server: {test}")
|
||||
if status != "verified," and self.passwd != "-1":
|
||||
raise LoginError("Password is incorrect")
|
||||
|
||||
if self.passwd == "-1":
|
||||
self.logger.info("Login successful (receive only)")
|
||||
else:
|
||||
self.logger.info("Login successful")
|
||||
|
||||
except LoginError as e:
|
||||
self.logger.error(str(e))
|
||||
self.close()
|
||||
raise
|
||||
except Exception as e:
|
||||
self.close()
|
||||
self.logger.error(f"Failed to login '{e}'")
|
||||
raise LoginError("Failed to login")
|
||||
|
||||
def consumer(self, callback, blocking=True, immortal=False, raw=False):
|
||||
"""
|
||||
When a position sentence is received, it will be passed to the callback function
|
||||
|
||||
blocking: if true (default), runs forever, otherwise will return after one sentence
|
||||
You can still exit the loop, by raising StopIteration in the callback function
|
||||
|
||||
immortal: When true, consumer will try to reconnect and stop propagation of Parse exceptions
|
||||
if false (default), consumer will return
|
||||
|
||||
raw: when true, raw packet is passed to callback, otherwise the result from aprs.parse()
|
||||
"""
|
||||
|
||||
if not self._connected:
|
||||
raise ConnectionError("not connected to a server")
|
||||
|
||||
line = b""
|
||||
|
||||
while True and not self.thread_stop:
|
||||
try:
|
||||
for line in self._socket_readlines(blocking):
|
||||
if line[0:1] != b"#":
|
||||
if raw:
|
||||
callback(line)
|
||||
else:
|
||||
callback(self._parse(line))
|
||||
else:
|
||||
self.logger.debug("Server: %s", line.decode("utf8"))
|
||||
stats.APRSDStats().set_aprsis_keepalive()
|
||||
except ParseError as exp:
|
||||
self.logger.log(
|
||||
11,
|
||||
"%s\n Packet: %s",
|
||||
exp,
|
||||
exp.packet,
|
||||
)
|
||||
except UnknownFormat as exp:
|
||||
self.logger.log(
|
||||
9,
|
||||
"%s\n Packet: %s",
|
||||
exp,
|
||||
exp.packet,
|
||||
)
|
||||
except LoginError as exp:
|
||||
self.logger.error("%s: %s", exp.__class__.__name__, exp)
|
||||
except (KeyboardInterrupt, SystemExit):
|
||||
raise
|
||||
except (ConnectionDrop, ConnectionError):
|
||||
self.close()
|
||||
|
||||
if not immortal:
|
||||
raise
|
||||
else:
|
||||
self.connect(blocking=blocking)
|
||||
continue
|
||||
except GenericError:
|
||||
pass
|
||||
except StopIteration:
|
||||
break
|
||||
except Exception:
|
||||
self.logger.error("APRS Packet: %s", line)
|
||||
raise
|
||||
|
||||
if not blocking:
|
||||
break
|
||||
@@ -5,83 +5,20 @@ 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
|
||||
loop = asyncio.new_event_loop()
|
||||
asyncio.set_event_loop(loop)
|
||||
self.loop = asyncio.get_event_loop()
|
||||
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,
|
||||
@@ -131,10 +68,20 @@ class Aioax25Client:
|
||||
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 set_filter(self, filter):
|
||||
# This does nothing right now.
|
||||
pass
|
||||
|
||||
def consumer(self, callback, blocking=True, immortal=False, raw=False):
|
||||
callsign = self.config["kiss"]["callsign"]
|
||||
call = callsign.split("-")
|
||||
if len(call) > 1:
|
||||
callsign = call[0]
|
||||
ssid = int(call[1])
|
||||
else:
|
||||
ssid = 0
|
||||
self.aprsint.bind(callback=callback, callsign=callsign, ssid=ssid, regex=False)
|
||||
self.loop.run_forever()
|
||||
|
||||
def send(self, msg):
|
||||
"""Send an APRS Message object."""
|
||||
@@ -145,8 +92,3 @@ class Aioax25Client:
|
||||
path=["WIDE1-1", "WIDE2-1"],
|
||||
oneshot=True,
|
||||
)
|
||||
|
||||
|
||||
def get_client():
|
||||
cl = KISSClient()
|
||||
return cl.client
|
||||
@@ -0,0 +1,36 @@
|
||||
import click
|
||||
import click_completion
|
||||
|
||||
from ..aprsd import cli
|
||||
|
||||
|
||||
CONTEXT_SETTINGS = dict(help_option_names=["-h", "--help"])
|
||||
|
||||
|
||||
@cli.group(help="Click Completion subcommands", context_settings=CONTEXT_SETTINGS)
|
||||
@click.pass_context
|
||||
def completion(ctx):
|
||||
pass
|
||||
|
||||
|
||||
# show dumps out the completion code for a particular shell
|
||||
@completion.command(help="Show completion code for shell", name="show")
|
||||
@click.option("-i", "--case-insensitive/--no-case-insensitive", help="Case insensitive completion")
|
||||
@click.argument("shell", required=False, type=click_completion.DocumentedChoice(click_completion.core.shells))
|
||||
def show(shell, case_insensitive):
|
||||
"""Show the click-completion-command completion code"""
|
||||
extra_env = {"_CLICK_COMPLETION_COMMAND_CASE_INSENSITIVE_COMPLETE": "ON"} if case_insensitive else {}
|
||||
click.echo(click_completion.core.get_code(shell, extra_env=extra_env))
|
||||
|
||||
|
||||
# install will install the completion code for a particular shell
|
||||
@completion.command(help="Install completion code for a shell", name="install")
|
||||
@click.option("--append/--overwrite", help="Append the completion code to the file", default=None)
|
||||
@click.option("-i", "--case-insensitive/--no-case-insensitive", help="Case insensitive completion")
|
||||
@click.argument("shell", required=False, type=click_completion.DocumentedChoice(click_completion.core.shells))
|
||||
@click.argument("path", required=False)
|
||||
def install(append, case_insensitive, shell, path):
|
||||
"""Install the click-completion-command completion"""
|
||||
extra_env = {"_CLICK_COMPLETION_COMMAND_CASE_INSENSITIVE_COMPLETE": "ON"} if case_insensitive else {}
|
||||
shell, path = click_completion.core.install(shell=shell, path=path, append=append, extra_env=extra_env)
|
||||
click.echo(f"{shell} completion installed in {path}")
|
||||
@@ -0,0 +1,118 @@
|
||||
#
|
||||
# Dev.py is used to help develop plugins
|
||||
#
|
||||
#
|
||||
# python included libs
|
||||
import logging
|
||||
|
||||
import click
|
||||
|
||||
# local imports here
|
||||
from aprsd import cli_helper, client, plugin
|
||||
|
||||
from ..aprsd import cli
|
||||
|
||||
|
||||
LOG = logging.getLogger("APRSD")
|
||||
CONTEXT_SETTINGS = dict(help_option_names=["-h", "--help"])
|
||||
|
||||
|
||||
@cli.group(help="Development type subcommands", context_settings=CONTEXT_SETTINGS)
|
||||
@click.pass_context
|
||||
def dev(ctx):
|
||||
pass
|
||||
|
||||
|
||||
@dev.command()
|
||||
@cli_helper.add_options(cli_helper.common_options)
|
||||
@click.option(
|
||||
"--aprs-login",
|
||||
envvar="APRS_LOGIN",
|
||||
show_envvar=True,
|
||||
help="What callsign to send the message from.",
|
||||
)
|
||||
@click.option(
|
||||
"-p",
|
||||
"--plugin",
|
||||
"plugin_path",
|
||||
show_default=True,
|
||||
default=None,
|
||||
help="The plugin to run. Ex: aprsd.plugins.ping.PingPlugin",
|
||||
)
|
||||
@click.option(
|
||||
"-a",
|
||||
"--all",
|
||||
"load_all",
|
||||
show_default=True,
|
||||
is_flag=True,
|
||||
default=False,
|
||||
help="Load all the plugins in config?",
|
||||
)
|
||||
@click.option(
|
||||
"-n",
|
||||
"--num",
|
||||
"number",
|
||||
show_default=True,
|
||||
default=1,
|
||||
help="Number of times to call the plugin",
|
||||
)
|
||||
@click.argument("message", nargs=-1, required=True)
|
||||
@click.pass_context
|
||||
@cli_helper.process_standard_options
|
||||
def test_plugin(
|
||||
ctx,
|
||||
aprs_login,
|
||||
plugin_path,
|
||||
load_all,
|
||||
number,
|
||||
message,
|
||||
):
|
||||
"""Test an individual APRSD plugin given a python path."""
|
||||
config = ctx.obj["config"]
|
||||
fromcall = aprs_login
|
||||
|
||||
if not plugin_path:
|
||||
click.echo(ctx.get_help())
|
||||
click.echo("")
|
||||
ctx.fail("Failed to provide -p option to test a plugin")
|
||||
ctx.exit()
|
||||
|
||||
if type(message) is tuple:
|
||||
message = " ".join(message)
|
||||
client.Client(config)
|
||||
|
||||
pm = plugin.PluginManager(config)
|
||||
if load_all:
|
||||
pm.setup_plugins()
|
||||
else:
|
||||
pm._init()
|
||||
obj = pm._create_class(plugin_path, plugin.APRSDPluginBase, config=config)
|
||||
if not obj:
|
||||
click.echo(ctx.get_help())
|
||||
click.echo("")
|
||||
ctx.fail(f"Failed to create object from plugin path '{plugin_path}'")
|
||||
ctx.exit()
|
||||
|
||||
# Register the plugin they wanted tested.
|
||||
LOG.info(
|
||||
"Testing plugin {} Version {}".format(
|
||||
obj.__class__, obj.version,
|
||||
),
|
||||
)
|
||||
pm._pluggy_pm.register(obj)
|
||||
login = config["aprs"]["login"]
|
||||
|
||||
packet = {
|
||||
"from": fromcall, "addresse": login,
|
||||
"message_text": message,
|
||||
"format": "message",
|
||||
"msgNo": 1,
|
||||
}
|
||||
LOG.info(f"P'{plugin_path}' F'{fromcall}' C'{message}'")
|
||||
|
||||
for x in range(number):
|
||||
reply = pm.run(packet)
|
||||
# Plugin might have threads, so lets stop them so we can exit.
|
||||
# obj.stop_threads()
|
||||
LOG.info(f"Result{x} = '{reply}'")
|
||||
pm.stop()
|
||||
@@ -0,0 +1,78 @@
|
||||
#
|
||||
# Used to fetch the stats url and determine if
|
||||
# aprsd server is 'healthy'
|
||||
#
|
||||
#
|
||||
# python included libs
|
||||
import datetime
|
||||
import json
|
||||
import logging
|
||||
import sys
|
||||
|
||||
import click
|
||||
import requests
|
||||
|
||||
import aprsd
|
||||
from aprsd import cli_helper, utils
|
||||
|
||||
# local imports here
|
||||
from ..aprsd import cli
|
||||
|
||||
|
||||
# setup the global logger
|
||||
# logging.basicConfig(level=logging.DEBUG) # level=10
|
||||
LOG = logging.getLogger("APRSD")
|
||||
|
||||
|
||||
@cli.command()
|
||||
@cli_helper.add_options(cli_helper.common_options)
|
||||
@click.option(
|
||||
"--url",
|
||||
"health_url",
|
||||
show_default=True,
|
||||
default="http://localhost:8001/stats",
|
||||
help="The aprsd url to call for checking health/stats",
|
||||
)
|
||||
@click.option(
|
||||
"--timeout",
|
||||
show_default=True,
|
||||
default=3,
|
||||
help="How long to wait for healtcheck url to come back",
|
||||
)
|
||||
@click.pass_context
|
||||
@cli_helper.process_standard_options_no_config
|
||||
def healthcheck(ctx, health_url, timeout):
|
||||
"""Check the health of the running aprsd server."""
|
||||
LOG.debug(f"APRSD HealthCheck version: {aprsd.__version__}")
|
||||
|
||||
try:
|
||||
url = health_url
|
||||
response = requests.get(url, timeout=timeout)
|
||||
response.raise_for_status()
|
||||
except Exception as ex:
|
||||
LOG.error(f"Failed to fetch healthcheck url '{url}' : '{ex}'")
|
||||
sys.exit(-1)
|
||||
else:
|
||||
stats = json.loads(response.text)
|
||||
LOG.debug(stats)
|
||||
|
||||
email_thread_last_update = stats["stats"]["email"]["thread_last_update"]
|
||||
|
||||
delta = utils.parse_delta_str(email_thread_last_update)
|
||||
d = datetime.timedelta(**delta)
|
||||
max_timeout = {"hours": 0.0, "minutes": 5, "seconds": 0}
|
||||
max_delta = datetime.timedelta(**max_timeout)
|
||||
if d > max_delta:
|
||||
LOG.error(f"Email thread is very old! {d}")
|
||||
sys.exit(-1)
|
||||
|
||||
aprsis_last_update = stats["stats"]["aprs-is"]["last_update"]
|
||||
delta = utils.parse_delta_str(aprsis_last_update)
|
||||
d = datetime.timedelta(**delta)
|
||||
max_timeout = {"hours": 0.0, "minutes": 5, "seconds": 0}
|
||||
max_delta = datetime.timedelta(**max_timeout)
|
||||
if d > max_delta:
|
||||
LOG.error(f"APRS-IS last update is very old! {d}")
|
||||
sys.exit(-1)
|
||||
|
||||
sys.exit(0)
|
||||
@@ -0,0 +1,59 @@
|
||||
import inspect
|
||||
import logging
|
||||
from textwrap import indent
|
||||
|
||||
import click
|
||||
from tabulate import tabulate
|
||||
|
||||
from aprsd import cli_helper, plugin
|
||||
from aprsd.plugins import (
|
||||
email, fortune, location, notify, ping, query, time, version, weather,
|
||||
)
|
||||
|
||||
from ..aprsd import cli
|
||||
|
||||
|
||||
LOG = logging.getLogger("APRSD")
|
||||
|
||||
|
||||
@cli.command()
|
||||
@cli_helper.add_options(cli_helper.common_options)
|
||||
@click.pass_context
|
||||
@cli_helper.process_standard_options_no_config
|
||||
def list_plugins(ctx):
|
||||
"""List the built in plugins available to APRSD."""
|
||||
|
||||
modules = [email, fortune, location, notify, ping, query, time, version, weather]
|
||||
plugins = []
|
||||
|
||||
for module in modules:
|
||||
entries = inspect.getmembers(module, inspect.isclass)
|
||||
for entry in entries:
|
||||
cls = entry[1]
|
||||
if issubclass(cls, plugin.APRSDPluginBase):
|
||||
info = {
|
||||
"name": cls.__qualname__,
|
||||
"path": f"{cls.__module__}.{cls.__qualname__}",
|
||||
"version": cls.version,
|
||||
"docstring": cls.__doc__,
|
||||
"short_desc": cls.short_description,
|
||||
}
|
||||
|
||||
if issubclass(cls, plugin.APRSDRegexCommandPluginBase):
|
||||
info["command_regex"] = cls.command_regex
|
||||
info["type"] = "RegexCommand"
|
||||
|
||||
if issubclass(cls, plugin.APRSDWatchListPluginBase):
|
||||
info["type"] = "WatchList"
|
||||
|
||||
plugins.append(info)
|
||||
|
||||
lines = []
|
||||
headers = ("Plugin Name", "Plugin Path", "Type", "Info")
|
||||
|
||||
for entry in plugins:
|
||||
lines.append(
|
||||
(entry["name"], entry["path"], entry["type"], entry["short_desc"]),
|
||||
)
|
||||
|
||||
click.echo(indent(tabulate(lines, headers, disable_numparse=True), " "))
|
||||
@@ -0,0 +1,161 @@
|
||||
#
|
||||
# License GPLv2
|
||||
#
|
||||
|
||||
# python included libs
|
||||
import datetime
|
||||
import logging
|
||||
import sys
|
||||
import time
|
||||
|
||||
import aprslib
|
||||
import click
|
||||
|
||||
# local imports here
|
||||
import aprsd
|
||||
from aprsd import (
|
||||
cli_helper, client, messaging, packets, stats, threads, trace, utils,
|
||||
)
|
||||
|
||||
from ..aprsd import cli
|
||||
|
||||
|
||||
# setup the global logger
|
||||
# logging.basicConfig(level=logging.DEBUG) # level=10
|
||||
LOG = logging.getLogger("APRSD")
|
||||
|
||||
|
||||
def signal_handler(sig, frame):
|
||||
threads.APRSDThreadList().stop_all()
|
||||
if "subprocess" not in str(frame):
|
||||
LOG.info(
|
||||
"Ctrl+C, Sending all threads exit! Can take up to 10 seconds {}".format(
|
||||
datetime.datetime.now(),
|
||||
),
|
||||
)
|
||||
time.sleep(5)
|
||||
LOG.info(stats.APRSDStats())
|
||||
|
||||
|
||||
@cli.command()
|
||||
@cli_helper.add_options(cli_helper.common_options)
|
||||
@click.option(
|
||||
"--aprs-login",
|
||||
envvar="APRS_LOGIN",
|
||||
show_envvar=True,
|
||||
help="What callsign to send the message from.",
|
||||
)
|
||||
@click.option(
|
||||
"--aprs-password",
|
||||
envvar="APRS_PASSWORD",
|
||||
show_envvar=True,
|
||||
help="the APRS-IS password for APRS_LOGIN",
|
||||
)
|
||||
@click.argument(
|
||||
"filter",
|
||||
nargs=-1,
|
||||
required=True,
|
||||
)
|
||||
@click.pass_context
|
||||
@cli_helper.process_standard_options
|
||||
def listen(
|
||||
ctx,
|
||||
aprs_login,
|
||||
aprs_password,
|
||||
filter,
|
||||
):
|
||||
"""Listen to packets on the APRS-IS Network based on FILTER.
|
||||
|
||||
FILTER is the APRS Filter to use.\n
|
||||
see http://www.aprs-is.net/javAPRSFilter.aspx\n
|
||||
r/lat/lon/dist - Range Filter Pass posits and objects within dist km from lat/lon.\n
|
||||
p/aa/bb/cc... - Prefix Filter Pass traffic with fromCall that start with aa or bb or cc.\n
|
||||
b/call1/call2... - Budlist Filter Pass all traffic from exact call: call1, call2, ... (* wild card allowed) \n
|
||||
o/obj1/obj2... - Object Filter Pass all objects with the exact name of obj1, obj2, ... (* wild card allowed)\n
|
||||
|
||||
"""
|
||||
config = ctx.obj["config"]
|
||||
|
||||
if not aprs_login:
|
||||
click.echo(ctx.get_help())
|
||||
click.echo("")
|
||||
ctx.fail("Must set --aprs_login or APRS_LOGIN")
|
||||
ctx.exit()
|
||||
|
||||
if not aprs_password:
|
||||
click.echo(ctx.get_help())
|
||||
click.echo("")
|
||||
ctx.fail("Must set --aprs-password or APRS_PASSWORD")
|
||||
ctx.exit()
|
||||
|
||||
config["aprs"]["login"] = aprs_login
|
||||
config["aprs"]["password"] = aprs_password
|
||||
|
||||
LOG.info(f"APRSD Listen Started version: {aprsd.__version__}")
|
||||
|
||||
flat_config = utils.flatten_dict(config)
|
||||
LOG.info("Using CONFIG values:")
|
||||
for x in flat_config:
|
||||
if "password" in x or "aprsd.web.users.admin" in x:
|
||||
LOG.info(f"{x} = XXXXXXXXXXXXXXXXXXX")
|
||||
else:
|
||||
LOG.info(f"{x} = {flat_config[x]}")
|
||||
|
||||
stats.APRSDStats(config)
|
||||
|
||||
# Try and load saved MsgTrack list
|
||||
LOG.debug("Loading saved MsgTrack object.")
|
||||
messaging.MsgTrack(config=config).load()
|
||||
packets.WatchList(config=config).load()
|
||||
packets.SeenList(config=config).load()
|
||||
|
||||
@trace.trace
|
||||
def rx_packet(packet):
|
||||
resp = packet.get("response", None)
|
||||
if resp == "ack":
|
||||
ack_num = packet.get("msgNo")
|
||||
LOG.info(f"We saw an ACK {ack_num} Ignoring")
|
||||
messaging.log_packet(packet)
|
||||
else:
|
||||
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,
|
||||
)
|
||||
|
||||
# Initialize the client factory and create
|
||||
# The correct client object ready for use
|
||||
client.ClientFactory.setup(config)
|
||||
# Make sure we have 1 client transport enabled
|
||||
if not client.factory.is_client_enabled():
|
||||
LOG.error("No Clients are enabled in config.")
|
||||
sys.exit(-1)
|
||||
|
||||
# Creates the client object
|
||||
LOG.info("Creating client connection")
|
||||
client.factory.create().client
|
||||
aprs_client = client.factory.create().client
|
||||
|
||||
LOG.debug(f"Filter by '{filter}'")
|
||||
aprs_client.set_filter(filter)
|
||||
|
||||
while True:
|
||||
try:
|
||||
# This will register a packet consumer with aprslib
|
||||
# When new packets come in the consumer will process
|
||||
# the packet
|
||||
aprs_client.consumer(rx_packet, raw=False)
|
||||
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
|
||||
aprs_client.reset()
|
||||
except aprslib.exceptions.UnknownFormat:
|
||||
LOG.error("Got a Bad packet")
|
||||
@@ -0,0 +1,165 @@
|
||||
import logging
|
||||
import sys
|
||||
import time
|
||||
|
||||
import aprslib
|
||||
from aprslib.exceptions import LoginError
|
||||
import click
|
||||
|
||||
import aprsd
|
||||
from aprsd import cli_helper, client, messaging, packets
|
||||
|
||||
from ..aprsd import cli
|
||||
|
||||
|
||||
LOG = logging.getLogger("APRSD")
|
||||
|
||||
|
||||
@cli.command()
|
||||
@cli_helper.add_options(cli_helper.common_options)
|
||||
@click.option(
|
||||
"--aprs-login",
|
||||
envvar="APRS_LOGIN",
|
||||
show_envvar=True,
|
||||
help="What callsign to send the message from.",
|
||||
)
|
||||
@click.option(
|
||||
"--aprs-password",
|
||||
envvar="APRS_PASSWORD",
|
||||
show_envvar=True,
|
||||
help="the APRS-IS password for APRS_LOGIN",
|
||||
)
|
||||
@click.option(
|
||||
"--no-ack",
|
||||
"-n",
|
||||
is_flag=True,
|
||||
show_default=True,
|
||||
default=False,
|
||||
help="Don't wait for an ack, just sent it to APRS-IS and bail.",
|
||||
)
|
||||
@click.option(
|
||||
"--wait-response",
|
||||
"-w",
|
||||
is_flag=True,
|
||||
show_default=True,
|
||||
default=False,
|
||||
help="Wait for a response to the message?",
|
||||
)
|
||||
@click.option("--raw", default=None, help="Send a raw message. Implies --no-ack")
|
||||
@click.argument("tocallsign", required=True)
|
||||
@click.argument("command", nargs=-1, required=True)
|
||||
@click.pass_context
|
||||
@cli_helper.process_standard_options
|
||||
def send_message(
|
||||
ctx,
|
||||
aprs_login,
|
||||
aprs_password,
|
||||
no_ack,
|
||||
wait_response,
|
||||
raw,
|
||||
tocallsign,
|
||||
command,
|
||||
):
|
||||
"""Send a message to a callsign via APRS_IS."""
|
||||
global got_ack, got_response
|
||||
config = ctx.obj["config"]
|
||||
quiet = ctx.obj["quiet"]
|
||||
|
||||
if not aprs_login:
|
||||
click.echo("Must set --aprs_login or APRS_LOGIN")
|
||||
return
|
||||
|
||||
if not aprs_password:
|
||||
click.echo("Must set --aprs-password or APRS_PASSWORD")
|
||||
return
|
||||
|
||||
config["aprs"]["login"] = aprs_login
|
||||
config["aprs"]["password"] = aprs_password
|
||||
|
||||
LOG.info(f"APRSD LISTEN Started version: {aprsd.__version__}")
|
||||
if type(command) is tuple:
|
||||
command = " ".join(command)
|
||||
if not quiet:
|
||||
if raw:
|
||||
LOG.info(f"L'{aprs_login}' R'{raw}'")
|
||||
else:
|
||||
LOG.info(f"L'{aprs_login}' To'{tocallsign}' C'{command}'")
|
||||
|
||||
packets.PacketList(config=config)
|
||||
packets.WatchList(config=config)
|
||||
packets.SeenList(config=config)
|
||||
|
||||
got_ack = False
|
||||
got_response = False
|
||||
|
||||
def rx_packet(packet):
|
||||
global got_ack, got_response
|
||||
# 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)
|
||||
got_ack = True
|
||||
else:
|
||||
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,
|
||||
)
|
||||
got_response = True
|
||||
# Send the ack back?
|
||||
ack = messaging.AckMessage(
|
||||
config["aprs"]["login"],
|
||||
fromcall,
|
||||
msg_id=msg_number,
|
||||
)
|
||||
ack.send_direct()
|
||||
|
||||
if got_ack:
|
||||
if wait_response:
|
||||
if got_response:
|
||||
sys.exit(0)
|
||||
else:
|
||||
sys.exit(0)
|
||||
|
||||
try:
|
||||
client.ClientFactory.setup(config)
|
||||
client.factory.create().client
|
||||
except LoginError:
|
||||
sys.exit(-1)
|
||||
|
||||
# Send a message
|
||||
# then we setup a consumer to rx messages
|
||||
# We should get an ack back as well as a new message
|
||||
# we should bail after we get the ack and send an ack back for the
|
||||
# message
|
||||
if raw:
|
||||
msg = messaging.RawMessage(raw)
|
||||
msg.send_direct()
|
||||
sys.exit(0)
|
||||
else:
|
||||
msg = messaging.TextMessage(aprs_login, tocallsign, command)
|
||||
msg.send_direct()
|
||||
|
||||
if no_ack:
|
||||
sys.exit(0)
|
||||
|
||||
try:
|
||||
# This will register a packet consumer with aprslib
|
||||
# When new packets come in the consumer will process
|
||||
# the packet
|
||||
aprs_client = client.factory.create().client
|
||||
aprs_client.consumer(rx_packet, raw=False)
|
||||
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
|
||||
aprs_client.reset()
|
||||
@@ -0,0 +1,121 @@
|
||||
import logging
|
||||
import signal
|
||||
import sys
|
||||
|
||||
import click
|
||||
|
||||
import aprsd
|
||||
from aprsd import (
|
||||
cli_helper, client, flask, messaging, packets, plugin, stats, threads,
|
||||
trace, utils,
|
||||
)
|
||||
from aprsd import aprsd as aprsd_main
|
||||
|
||||
from ..aprsd import cli
|
||||
|
||||
|
||||
LOG = logging.getLogger("APRSD")
|
||||
|
||||
|
||||
# main() ###
|
||||
@cli.command()
|
||||
@cli_helper.add_options(cli_helper.common_options)
|
||||
@click.option(
|
||||
"-f",
|
||||
"--flush",
|
||||
"flush",
|
||||
is_flag=True,
|
||||
show_default=True,
|
||||
default=False,
|
||||
help="Flush out all old aged messages on disk.",
|
||||
)
|
||||
@click.pass_context
|
||||
@cli_helper.process_standard_options
|
||||
def server(ctx, flush):
|
||||
"""Start the aprsd server gateway process."""
|
||||
ctx.obj["config_file"]
|
||||
loglevel = ctx.obj["loglevel"]
|
||||
quiet = ctx.obj["quiet"]
|
||||
config = ctx.obj["config"]
|
||||
|
||||
signal.signal(signal.SIGINT, aprsd_main.signal_handler)
|
||||
signal.signal(signal.SIGTERM, aprsd_main.signal_handler)
|
||||
|
||||
if not quiet:
|
||||
click.echo("Load config")
|
||||
|
||||
level, msg = utils._check_version()
|
||||
if level:
|
||||
LOG.warning(msg)
|
||||
else:
|
||||
LOG.info(msg)
|
||||
LOG.info(f"APRSD Started version: {aprsd.__version__}")
|
||||
|
||||
flat_config = utils.flatten_dict(config)
|
||||
LOG.info("Using CONFIG values:")
|
||||
for x in flat_config:
|
||||
if "password" in x or "aprsd.web.users.admin" in x:
|
||||
LOG.info(f"{x} = XXXXXXXXXXXXXXXXXXX")
|
||||
else:
|
||||
LOG.info(f"{x} = {flat_config[x]}")
|
||||
|
||||
if config["aprsd"].get("trace", False):
|
||||
trace.setup_tracing(["method", "api"])
|
||||
stats.APRSDStats(config)
|
||||
|
||||
# Initialize the client factory and create
|
||||
# The correct client object ready for use
|
||||
client.ClientFactory.setup(config)
|
||||
# Make sure we have 1 client transport enabled
|
||||
if not client.factory.is_client_enabled():
|
||||
LOG.error("No Clients are enabled in config.")
|
||||
sys.exit(-1)
|
||||
|
||||
# Creates the client object
|
||||
LOG.info("Creating client connection")
|
||||
client.factory.create().client
|
||||
|
||||
# Now load the msgTrack from disk if any
|
||||
packets.PacketList(config=config)
|
||||
if flush:
|
||||
LOG.debug("Deleting saved MsgTrack.")
|
||||
messaging.MsgTrack(config=config).flush()
|
||||
packets.WatchList(config=config)
|
||||
packets.SeenList(config=config)
|
||||
else:
|
||||
# Try and load saved MsgTrack list
|
||||
LOG.debug("Loading saved MsgTrack object.")
|
||||
messaging.MsgTrack(config=config).load()
|
||||
packets.WatchList(config=config).load()
|
||||
packets.SeenList(config=config).load()
|
||||
|
||||
# Create the initial PM singleton and Register plugins
|
||||
LOG.info("Loading Plugin Manager and registering plugins")
|
||||
plugin_manager = plugin.PluginManager(config)
|
||||
plugin_manager.setup_plugins()
|
||||
|
||||
rx_thread = threads.APRSDRXThread(
|
||||
msg_queues=threads.msg_queues,
|
||||
config=config,
|
||||
)
|
||||
rx_thread.start()
|
||||
|
||||
messaging.MsgTrack().restart()
|
||||
|
||||
keepalive = threads.KeepAliveThread(config=config)
|
||||
keepalive.start()
|
||||
|
||||
web_enabled = config.get("aprsd.web.enabled", default=False)
|
||||
|
||||
if web_enabled:
|
||||
aprsd_main.flask_enabled = True
|
||||
(socketio, app) = flask.init_flask(config, loglevel, quiet)
|
||||
socketio.run(
|
||||
app,
|
||||
host=config["aprsd"]["web"]["host"],
|
||||
port=config["aprsd"]["web"]["port"],
|
||||
)
|
||||
|
||||
# If there are items in the msgTracker, then save them
|
||||
LOG.info("APRSD Exiting.")
|
||||
return 0
|
||||
+385
@@ -0,0 +1,385 @@
|
||||
import collections
|
||||
import logging
|
||||
import os
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
import click
|
||||
import yaml
|
||||
|
||||
from aprsd import utils
|
||||
|
||||
|
||||
home = str(Path.home())
|
||||
DEFAULT_CONFIG_DIR = f"{home}/.config/aprsd/"
|
||||
DEFAULT_SAVE_FILE = f"{home}/.config/aprsd/aprsd.p"
|
||||
DEFAULT_CONFIG_FILE = f"{home}/.config/aprsd/aprsd.yml"
|
||||
|
||||
|
||||
LOG_LEVELS = {
|
||||
"CRITICAL": logging.CRITICAL,
|
||||
"ERROR": logging.ERROR,
|
||||
"WARNING": logging.WARNING,
|
||||
"INFO": logging.INFO,
|
||||
"DEBUG": logging.DEBUG,
|
||||
}
|
||||
|
||||
DEFAULT_DATE_FORMAT = "%m/%d/%Y %I:%M:%S %p"
|
||||
DEFAULT_LOG_FORMAT = (
|
||||
"[%(asctime)s] [%(threadName)-20.20s] [%(levelname)-5.5s]"
|
||||
" %(message)s - [%(pathname)s:%(lineno)d]"
|
||||
)
|
||||
|
||||
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]"
|
||||
)
|
||||
|
||||
CORE_MESSAGE_PLUGINS = [
|
||||
"aprsd.plugins.email.EmailPlugin",
|
||||
"aprsd.plugins.fortune.FortunePlugin",
|
||||
"aprsd.plugins.location.LocationPlugin",
|
||||
"aprsd.plugins.ping.PingPlugin",
|
||||
"aprsd.plugins.query.QueryPlugin",
|
||||
"aprsd.plugins.stock.StockPlugin",
|
||||
"aprsd.plugins.time.TimePlugin",
|
||||
"aprsd.plugins.weather.USWeatherPlugin",
|
||||
"aprsd.plugins.version.VersionPlugin",
|
||||
]
|
||||
|
||||
CORE_NOTIFY_PLUGINS = [
|
||||
"aprsd.plugins.notify.NotifySeenPlugin",
|
||||
]
|
||||
|
||||
# an example of what should be in the ~/.aprsd/config.yml
|
||||
DEFAULT_CONFIG_DICT = {
|
||||
"ham": {"callsign": "NOCALL"},
|
||||
"aprs": {
|
||||
"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,
|
||||
"dateformat": DEFAULT_DATE_FORMAT,
|
||||
"save_location": DEFAULT_CONFIG_DIR,
|
||||
"trace": False,
|
||||
"enabled_plugins": CORE_MESSAGE_PLUGINS,
|
||||
"units": "imperial",
|
||||
"watch_list": {
|
||||
"enabled": False,
|
||||
# Who gets the alert?
|
||||
"alert_callsign": "NOCALL",
|
||||
# 43200 is 12 hours
|
||||
"alert_time_seconds": 43200,
|
||||
# How many packets to save in a ring Buffer
|
||||
# for a particular callsign
|
||||
"packet_keep_count": 10,
|
||||
"callsigns": [],
|
||||
"enabled_plugins": CORE_NOTIFY_PLUGINS,
|
||||
},
|
||||
"web": {
|
||||
"enabled": True,
|
||||
"logging_enabled": True,
|
||||
"host": "0.0.0.0",
|
||||
"port": 8001,
|
||||
"users": {
|
||||
"admin": "password-here",
|
||||
},
|
||||
},
|
||||
"email": {
|
||||
"enabled": True,
|
||||
"shortcuts": {
|
||||
"aa": "5551239999@vtext.com",
|
||||
"cl": "craiglamparter@somedomain.org",
|
||||
"wb": "555309@vtext.com",
|
||||
},
|
||||
"smtp": {
|
||||
"login": "SMTP_USERNAME",
|
||||
"password": "SMTP_PASSWORD",
|
||||
"host": "smtp.gmail.com",
|
||||
"port": 465,
|
||||
"use_ssl": False,
|
||||
"debug": False,
|
||||
},
|
||||
"imap": {
|
||||
"login": "IMAP_USERNAME",
|
||||
"password": "IMAP_PASSWORD",
|
||||
"host": "imap.gmail.com",
|
||||
"port": 993,
|
||||
"use_ssl": True,
|
||||
"debug": False,
|
||||
},
|
||||
},
|
||||
},
|
||||
"services": {
|
||||
"aprs.fi": {"apiKey": "APIKEYVALUE"},
|
||||
"openweathermap": {"apiKey": "APIKEYVALUE"},
|
||||
"opencagedata": {"apiKey": "APIKEYVALUE"},
|
||||
"avwx": {"base_url": "http://host:port", "apiKey": "APIKEYVALUE"},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
class Config(collections.UserDict):
|
||||
def _get(self, d, keys, default=None):
|
||||
"""
|
||||
Example:
|
||||
d = {'meta': {'status': 'OK', 'status_code': 200}}
|
||||
_get(d, ['meta', 'status_code']) # => 200
|
||||
_get(d, ['garbage', 'status_code']) # => None
|
||||
_get(d, ['meta', 'garbage'], default='-') # => '-'
|
||||
|
||||
"""
|
||||
if type(keys) is str and "." in keys:
|
||||
keys = keys.split(".")
|
||||
|
||||
assert type(keys) is list
|
||||
if d is None:
|
||||
return default
|
||||
|
||||
if not keys:
|
||||
return d
|
||||
|
||||
if type(d) is str:
|
||||
return default
|
||||
|
||||
return self._get(d.get(keys[0]), keys[1:], default)
|
||||
|
||||
def get(self, path, default=None):
|
||||
return self._get(self.data, path, default=default)
|
||||
|
||||
def exists(self, path):
|
||||
"""See if a conf value exists."""
|
||||
test = "-3.14TEST41.3-"
|
||||
return self.get(path, default=test) != test
|
||||
|
||||
def check_option(self, path, default_fail=None):
|
||||
"""Make sure the config option doesn't have default value."""
|
||||
if not self.exists(path):
|
||||
raise Exception(
|
||||
"Option '{}' was not in config file".format(
|
||||
path,
|
||||
),
|
||||
)
|
||||
|
||||
val = self.get(path)
|
||||
if val == default_fail:
|
||||
# We have to fail and bail if the user hasn't edited
|
||||
# this config option.
|
||||
raise Exception(
|
||||
"Config file needs to be changed from provided"
|
||||
" defaults for '{}'".format(
|
||||
path,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def add_config_comments(raw_yaml):
|
||||
end_idx = utils.end_substr(raw_yaml, "aprs:")
|
||||
if end_idx != -1:
|
||||
# lets insert a comment
|
||||
raw_yaml = utils.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,
|
||||
)
|
||||
|
||||
end_idx = utils.end_substr(raw_yaml, "aprs.fi:")
|
||||
if end_idx != -1:
|
||||
# lets insert a comment
|
||||
raw_yaml = utils.insert_str(
|
||||
raw_yaml,
|
||||
"\n # Get the apiKey from your aprs.fi account here: "
|
||||
"\n # http://aprs.fi/account",
|
||||
end_idx,
|
||||
)
|
||||
|
||||
end_idx = utils.end_substr(raw_yaml, "opencagedata:")
|
||||
if end_idx != -1:
|
||||
# lets insert a comment
|
||||
raw_yaml = utils.insert_str(
|
||||
raw_yaml,
|
||||
"\n # (Optional for TimeOpenCageDataPlugin) "
|
||||
"\n # Get the apiKey from your opencagedata account here: "
|
||||
"\n # https://opencagedata.com/dashboard#api-keys",
|
||||
end_idx,
|
||||
)
|
||||
|
||||
end_idx = utils.end_substr(raw_yaml, "openweathermap:")
|
||||
if end_idx != -1:
|
||||
# lets insert a comment
|
||||
raw_yaml = utils.insert_str(
|
||||
raw_yaml,
|
||||
"\n # (Optional for OWMWeatherPlugin) "
|
||||
"\n # Get the apiKey from your "
|
||||
"\n # openweathermap account here: "
|
||||
"\n # https://home.openweathermap.org/api_keys",
|
||||
end_idx,
|
||||
)
|
||||
|
||||
end_idx = utils.end_substr(raw_yaml, "avwx:")
|
||||
if end_idx != -1:
|
||||
# lets insert a comment
|
||||
raw_yaml = utils.insert_str(
|
||||
raw_yaml,
|
||||
"\n # (Optional for AVWXWeatherPlugin) "
|
||||
"\n # Use hosted avwx-api here: https://avwx.rest "
|
||||
"\n # or deploy your own from here: "
|
||||
"\n # https://github.com/avwx-rest/avwx-api",
|
||||
end_idx,
|
||||
)
|
||||
|
||||
return raw_yaml
|
||||
|
||||
|
||||
def dump_default_cfg():
|
||||
return add_config_comments(
|
||||
yaml.dump(
|
||||
DEFAULT_CONFIG_DICT,
|
||||
indent=4,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def create_default_config():
|
||||
"""Create a default config file."""
|
||||
# make sure the directory location exists
|
||||
config_file_expanded = os.path.expanduser(DEFAULT_CONFIG_FILE)
|
||||
config_dir = os.path.dirname(config_file_expanded)
|
||||
if not os.path.exists(config_dir):
|
||||
click.echo(f"Config dir '{config_dir}' doesn't exist, creating.")
|
||||
utils.mkdir_p(config_dir)
|
||||
with open(config_file_expanded, "w+") as cf:
|
||||
cf.write(dump_default_cfg())
|
||||
|
||||
|
||||
def get_config(config_file):
|
||||
"""This tries to read the yaml config from <config_file>."""
|
||||
config_file_expanded = os.path.expanduser(config_file)
|
||||
if os.path.exists(config_file_expanded):
|
||||
with open(config_file_expanded) as stream:
|
||||
config = yaml.load(stream, Loader=yaml.FullLoader)
|
||||
return Config(config)
|
||||
else:
|
||||
if config_file == DEFAULT_CONFIG_FILE:
|
||||
click.echo(
|
||||
f"{config_file_expanded} is missing, creating config file",
|
||||
)
|
||||
create_default_config()
|
||||
msg = (
|
||||
"Default config file created at {}. Please edit with your "
|
||||
"settings.".format(config_file)
|
||||
)
|
||||
click.echo(msg)
|
||||
else:
|
||||
# The user provided a config file path different from the
|
||||
# Default, so we won't try and create it, just bitch and bail.
|
||||
msg = f"Custom config file '{config_file}' is missing."
|
||||
click.echo(msg)
|
||||
|
||||
sys.exit(-1)
|
||||
|
||||
|
||||
# This method tries to parse the config yaml file
|
||||
# and consume the settings.
|
||||
# If the required params don't exist,
|
||||
# it will look in the environment
|
||||
def parse_config(config_file):
|
||||
config = get_config(config_file)
|
||||
|
||||
def fail(msg):
|
||||
click.echo(msg)
|
||||
sys.exit(-1)
|
||||
|
||||
def check_option(config, path, default_fail=None):
|
||||
try:
|
||||
config.check_option(path, default_fail=default_fail)
|
||||
except Exception as ex:
|
||||
fail(repr(ex))
|
||||
else:
|
||||
return config
|
||||
|
||||
# special check here to make sure user has edited the config file
|
||||
# and changed the ham callsign
|
||||
check_option(
|
||||
config,
|
||||
"ham.callsign",
|
||||
default_fail=DEFAULT_CONFIG_DICT["ham"]["callsign"],
|
||||
)
|
||||
check_option(
|
||||
config,
|
||||
"aprs.login",
|
||||
default_fail=DEFAULT_CONFIG_DICT["aprs"]["login"],
|
||||
)
|
||||
check_option(
|
||||
config,
|
||||
["aprs", "password"],
|
||||
default_fail=DEFAULT_CONFIG_DICT["aprs"]["password"],
|
||||
)
|
||||
|
||||
# Ensure they change the admin password
|
||||
if config.get("aprsd.web.enabled") is True:
|
||||
check_option(
|
||||
config,
|
||||
["aprsd", "web", "users", "admin"],
|
||||
default_fail=DEFAULT_CONFIG_DICT["aprsd"]["web"]["users"]["admin"],
|
||||
)
|
||||
|
||||
if config.get("aprsd.watch_list.enabled") is True:
|
||||
check_option(
|
||||
config,
|
||||
["aprsd", "watch_list", "alert_callsign"],
|
||||
default_fail=DEFAULT_CONFIG_DICT["aprsd"]["watch_list"]["alert_callsign"],
|
||||
)
|
||||
|
||||
if config.get("aprsd.email.enabled") is True:
|
||||
# Check IMAP server settings
|
||||
check_option(config, ["aprsd", "email", "imap", "host"])
|
||||
check_option(config, ["aprsd", "email", "imap", "port"])
|
||||
check_option(
|
||||
config,
|
||||
["aprsd", "email", "imap", "login"],
|
||||
default_fail=DEFAULT_CONFIG_DICT["aprsd"]["email"]["imap"]["login"],
|
||||
)
|
||||
check_option(
|
||||
config,
|
||||
["aprsd", "email", "imap", "password"],
|
||||
default_fail=DEFAULT_CONFIG_DICT["aprsd"]["email"]["imap"]["password"],
|
||||
)
|
||||
|
||||
# Check SMTP server settings
|
||||
check_option(config, ["aprsd", "email", "smtp", "host"])
|
||||
check_option(config, ["aprsd", "email", "smtp", "port"])
|
||||
check_option(
|
||||
config,
|
||||
["aprsd", "email", "smtp", "login"],
|
||||
default_fail=DEFAULT_CONFIG_DICT["aprsd"]["email"]["smtp"]["login"],
|
||||
)
|
||||
check_option(
|
||||
config,
|
||||
["aprsd", "email", "smtp", "password"],
|
||||
default_fail=DEFAULT_CONFIG_DICT["aprsd"]["email"]["smtp"]["password"],
|
||||
)
|
||||
|
||||
return config
|
||||
-208
@@ -1,208 +0,0 @@
|
||||
#
|
||||
# Dev.py is used to help develop plugins
|
||||
#
|
||||
#
|
||||
# python included libs
|
||||
import logging
|
||||
from logging import NullHandler
|
||||
from logging.handlers import RotatingFileHandler
|
||||
import os
|
||||
import sys
|
||||
|
||||
import click
|
||||
import click_completion
|
||||
|
||||
# local imports here
|
||||
import aprsd
|
||||
from aprsd import client, plugin, utils
|
||||
|
||||
|
||||
# setup the global logger
|
||||
# logging.basicConfig(level=logging.DEBUG) # level=10
|
||||
LOG = logging.getLogger("APRSD")
|
||||
|
||||
LOG_LEVELS = {
|
||||
"CRITICAL": logging.CRITICAL,
|
||||
"ERROR": logging.ERROR,
|
||||
"WARNING": logging.WARNING,
|
||||
"INFO": logging.INFO,
|
||||
"DEBUG": logging.DEBUG,
|
||||
}
|
||||
|
||||
CONTEXT_SETTINGS = dict(help_option_names=["-h", "--help"])
|
||||
|
||||
|
||||
def custom_startswith(string, incomplete):
|
||||
"""A custom completion match that supports case insensitive matching."""
|
||||
if os.environ.get("_CLICK_COMPLETION_COMMAND_CASE_INSENSITIVE_COMPLETE"):
|
||||
string = string.lower()
|
||||
incomplete = incomplete.lower()
|
||||
return string.startswith(incomplete)
|
||||
|
||||
|
||||
click_completion.core.startswith = custom_startswith
|
||||
click_completion.init()
|
||||
|
||||
|
||||
cmd_help = """Shell completion for click-completion-command
|
||||
Available shell types:
|
||||
\b
|
||||
%s
|
||||
Default type: auto
|
||||
""" % "\n ".join(
|
||||
f"{k:<12} {click_completion.core.shells[k]}"
|
||||
for k in sorted(click_completion.core.shells.keys())
|
||||
)
|
||||
|
||||
|
||||
@click.group(help=cmd_help, context_settings=CONTEXT_SETTINGS)
|
||||
@click.version_option()
|
||||
def main():
|
||||
pass
|
||||
|
||||
|
||||
@main.command()
|
||||
@click.option(
|
||||
"-i",
|
||||
"--case-insensitive/--no-case-insensitive",
|
||||
help="Case insensitive completion",
|
||||
)
|
||||
@click.argument(
|
||||
"shell",
|
||||
required=False,
|
||||
type=click_completion.DocumentedChoice(click_completion.core.shells),
|
||||
)
|
||||
def show(shell, case_insensitive):
|
||||
"""Show the click-completion-command completion code"""
|
||||
extra_env = (
|
||||
{"_CLICK_COMPLETION_COMMAND_CASE_INSENSITIVE_COMPLETE": "ON"}
|
||||
if case_insensitive
|
||||
else {}
|
||||
)
|
||||
click.echo(click_completion.core.get_code(shell, extra_env=extra_env))
|
||||
|
||||
|
||||
@main.command()
|
||||
@click.option(
|
||||
"--append/--overwrite",
|
||||
help="Append the completion code to the file",
|
||||
default=None,
|
||||
)
|
||||
@click.option(
|
||||
"-i",
|
||||
"--case-insensitive/--no-case-insensitive",
|
||||
help="Case insensitive completion",
|
||||
)
|
||||
@click.argument(
|
||||
"shell",
|
||||
required=False,
|
||||
type=click_completion.DocumentedChoice(click_completion.core.shells),
|
||||
)
|
||||
@click.argument("path", required=False)
|
||||
def install(append, case_insensitive, shell, path):
|
||||
"""Install the click-completion-command completion"""
|
||||
extra_env = (
|
||||
{"_CLICK_COMPLETION_COMMAND_CASE_INSENSITIVE_COMPLETE": "ON"}
|
||||
if case_insensitive
|
||||
else {}
|
||||
)
|
||||
shell, path = click_completion.core.install(
|
||||
shell=shell,
|
||||
path=path,
|
||||
append=append,
|
||||
extra_env=extra_env,
|
||||
)
|
||||
click.echo(f"{shell} completion installed in {path}")
|
||||
|
||||
|
||||
# Setup the logging faciility
|
||||
# to disable logging to stdout, but still log to file
|
||||
# use the --quiet option on the cmdln
|
||||
def setup_logging(config, loglevel, quiet):
|
||||
log_level = LOG_LEVELS[loglevel]
|
||||
LOG.setLevel(log_level)
|
||||
log_format = "[%(asctime)s] [%(threadName)-12s] [%(levelname)-5.5s]" " %(message)s"
|
||||
date_format = "%m/%d/%Y %I:%M:%S %p"
|
||||
log_formatter = logging.Formatter(fmt=log_format, datefmt=date_format)
|
||||
log_file = config["aprs"].get("logfile", None)
|
||||
if log_file:
|
||||
fh = RotatingFileHandler(log_file, maxBytes=(10248576 * 5), backupCount=4)
|
||||
else:
|
||||
fh = NullHandler()
|
||||
|
||||
fh.setFormatter(log_formatter)
|
||||
LOG.addHandler(fh)
|
||||
|
||||
if not quiet:
|
||||
sh = logging.StreamHandler(sys.stdout)
|
||||
sh.setFormatter(log_formatter)
|
||||
LOG.addHandler(sh)
|
||||
|
||||
|
||||
@main.command()
|
||||
@click.option(
|
||||
"--loglevel",
|
||||
default="DEBUG",
|
||||
show_default=True,
|
||||
type=click.Choice(
|
||||
["CRITICAL", "ERROR", "WARNING", "INFO", "DEBUG"],
|
||||
case_sensitive=False,
|
||||
),
|
||||
show_choices=True,
|
||||
help="The log level to use for aprsd.log",
|
||||
)
|
||||
@click.option(
|
||||
"-c",
|
||||
"--config",
|
||||
"config_file",
|
||||
show_default=True,
|
||||
default=utils.DEFAULT_CONFIG_FILE,
|
||||
help="The aprsd config file to use for options.",
|
||||
)
|
||||
@click.option(
|
||||
"-p",
|
||||
"--plugin",
|
||||
"plugin_path",
|
||||
show_default=True,
|
||||
default="aprsd.plugins.wx.WxPlugin",
|
||||
help="The plugin to run",
|
||||
)
|
||||
@click.argument("fromcall")
|
||||
@click.argument("message", nargs=-1, required=True)
|
||||
def test_plugin(
|
||||
loglevel,
|
||||
config_file,
|
||||
plugin_path,
|
||||
fromcall,
|
||||
message,
|
||||
):
|
||||
"""APRSD Plugin test app."""
|
||||
|
||||
config = utils.parse_config(config_file)
|
||||
|
||||
setup_logging(config, loglevel, False)
|
||||
LOG.info(f"Test APRSD PLugin version: {aprsd.__version__}")
|
||||
if type(message) is tuple:
|
||||
message = " ".join(message)
|
||||
LOG.info(f"P'{plugin_path}' F'{fromcall}' C'{message}'")
|
||||
client.Client(config)
|
||||
|
||||
pm = plugin.PluginManager(config)
|
||||
obj = pm._create_class(plugin_path, plugin.APRSDPluginBase, config=config)
|
||||
login = config["aprs"]["login"]
|
||||
|
||||
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.
|
||||
obj.stop_threads()
|
||||
LOG.info(f"Result = '{reply}'")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,84 +0,0 @@
|
||||
import argparse
|
||||
import logging
|
||||
from logging.handlers import RotatingFileHandler
|
||||
import socketserver
|
||||
import sys
|
||||
import time
|
||||
|
||||
from aprsd import utils
|
||||
|
||||
|
||||
# command line args
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument(
|
||||
"--loglevel",
|
||||
default="DEBUG",
|
||||
choices=["CRITICAL", "ERROR", "WARNING", "INFO", "DEBUG"],
|
||||
help="The log level to use for aprsd.log",
|
||||
)
|
||||
parser.add_argument("--quiet", action="store_true", help="Don't log to stdout")
|
||||
|
||||
parser.add_argument("--port", default=9099, type=int, help="The port to listen on .")
|
||||
parser.add_argument("--ip", default="127.0.0.1", help="The IP to listen on ")
|
||||
|
||||
CONFIG = None
|
||||
LOG = logging.getLogger("ARPSSERVER")
|
||||
|
||||
|
||||
# Setup the logging faciility
|
||||
# to disable logging to stdout, but still log to file
|
||||
# use the --quiet option on the cmdln
|
||||
def setup_logging(args):
|
||||
global LOG
|
||||
levels = {
|
||||
"CRITICAL": logging.CRITICAL,
|
||||
"ERROR": logging.ERROR,
|
||||
"WARNING": logging.WARNING,
|
||||
"INFO": logging.INFO,
|
||||
"DEBUG": logging.DEBUG,
|
||||
}
|
||||
log_level = levels[args.loglevel]
|
||||
|
||||
LOG.setLevel(log_level)
|
||||
log_format = "%(asctime)s [%(threadName)-12.12s] [%(levelname)-5.5s]" " %(message)s"
|
||||
date_format = "%m/%d/%Y %I:%M:%S %p"
|
||||
log_formatter = logging.Formatter(fmt=log_format, datefmt=date_format)
|
||||
fh = RotatingFileHandler("aprs-server.log", maxBytes=(10248576 * 5), backupCount=4)
|
||||
fh.setFormatter(log_formatter)
|
||||
LOG.addHandler(fh)
|
||||
|
||||
if not args.quiet:
|
||||
sh = logging.StreamHandler(sys.stdout)
|
||||
sh.setFormatter(log_formatter)
|
||||
LOG.addHandler(sh)
|
||||
|
||||
|
||||
class MyAPRSTCPHandler(socketserver.BaseRequestHandler):
|
||||
def handle(self):
|
||||
# self.request is the TCP socket connected to the client
|
||||
self.data = self.request.recv(1024).strip()
|
||||
LOG.debug(f"{self.client_address[0]} wrote:")
|
||||
LOG.debug(self.data)
|
||||
# just send back the same data, but upper-cased
|
||||
self.request.sendall(self.data.upper())
|
||||
|
||||
|
||||
def main():
|
||||
global CONFIG
|
||||
args = parser.parse_args()
|
||||
setup_logging(args)
|
||||
LOG.info("Test APRS server starting.")
|
||||
time.sleep(1)
|
||||
|
||||
CONFIG = utils.parse_config(args)
|
||||
|
||||
ip = CONFIG["aprs"]["host"]
|
||||
port = CONFIG["aprs"]["port"]
|
||||
LOG.info(f"Start server listening on {args.ip}:{args.port}")
|
||||
|
||||
with socketserver.TCPServer((ip, port), MyAPRSTCPHandler) as server:
|
||||
server.serve_forever()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
+22
-16
@@ -17,9 +17,10 @@ from flask_socketio import Namespace, SocketIO
|
||||
from werkzeug.security import check_password_hash, generate_password_hash
|
||||
|
||||
import aprsd
|
||||
from aprsd import (
|
||||
client, kissclient, messaging, packets, plugin, stats, threads, utils,
|
||||
)
|
||||
from aprsd import client
|
||||
from aprsd import config as aprsd_config
|
||||
from aprsd import log, messaging, packets, plugin, stats, threads, utils
|
||||
from aprsd.clients import aprsis
|
||||
|
||||
|
||||
LOG = logging.getLogger("APRSD")
|
||||
@@ -136,7 +137,8 @@ class SendMessageThread(threads.APRSDThread):
|
||||
while not connected:
|
||||
try:
|
||||
LOG.info("Creating aprslib client")
|
||||
aprs_client = client.Aprsdis(
|
||||
|
||||
aprs_client = aprsis.Aprsdis(
|
||||
user,
|
||||
passwd=password,
|
||||
host=host,
|
||||
@@ -294,12 +296,15 @@ class APRSDFlask(flask_classful.FlaskView):
|
||||
)
|
||||
wl = packets.WatchList()
|
||||
if wl.is_enabled():
|
||||
watch_count = len(wl.callsigns)
|
||||
watch_count = len(wl)
|
||||
watch_age = wl.max_delta()
|
||||
else:
|
||||
watch_count = 0
|
||||
watch_age = 0
|
||||
|
||||
sl = packets.SeenList()
|
||||
seen_count = len(sl)
|
||||
|
||||
pm = plugin.PluginManager()
|
||||
plugins = pm.get_plugins()
|
||||
plugin_count = len(plugins)
|
||||
@@ -312,16 +317,16 @@ class APRSDFlask(flask_classful.FlaskView):
|
||||
)
|
||||
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:
|
||||
if client.KISSClient.kiss_enabled(self.config):
|
||||
transport = client.KISSClient.transport(self.config)
|
||||
if transport == client.TRANSPORT_TCPKISS:
|
||||
aprs_connection = (
|
||||
"TCPKISS://{}:{}".format(
|
||||
self.config["kiss"]["tcp"]["host"],
|
||||
self.config["kiss"]["tcp"]["port"],
|
||||
)
|
||||
)
|
||||
elif transport == kissclient.TRANSPORT_SERIALKISS:
|
||||
elif transport == client.TRANSPORT_SERIALKISS:
|
||||
aprs_connection = (
|
||||
"SerialKISS://{}@{} baud".format(
|
||||
self.config["kiss"]["serial"]["device"],
|
||||
@@ -338,9 +343,10 @@ class APRSDFlask(flask_classful.FlaskView):
|
||||
aprs_connection=aprs_connection,
|
||||
callsign=self.config["aprs"]["login"],
|
||||
version=aprsd.__version__,
|
||||
config_json=json.dumps(self.config),
|
||||
config_json=json.dumps(self.config.data),
|
||||
watch_count=watch_count,
|
||||
watch_age=watch_age,
|
||||
seen_count=seen_count,
|
||||
plugin_count=plugin_count,
|
||||
)
|
||||
|
||||
@@ -402,14 +408,14 @@ class APRSDFlask(flask_classful.FlaskView):
|
||||
# Convert the watch_list entries to age
|
||||
wl = packets.WatchList()
|
||||
new_list = {}
|
||||
for call in wl.callsigns:
|
||||
for call in wl.get_all():
|
||||
# call_date = datetime.datetime.strptime(
|
||||
# str(wl.last_seen(call)),
|
||||
# "%Y-%m-%d %H:%M:%S.%f",
|
||||
# )
|
||||
new_list[call] = {
|
||||
"last": wl.age(call),
|
||||
"packets": wl.callsigns[call]["packets"].get(),
|
||||
"packets": wl.get(call)["packets"].get(),
|
||||
}
|
||||
|
||||
stats_dict["aprsd"]["watch_list"] = new_list
|
||||
@@ -494,7 +500,7 @@ class LogMonitorThread(threads.APRSDThread):
|
||||
def loop(self):
|
||||
global socketio
|
||||
try:
|
||||
record = threads.logging_queue.get(block=True, timeout=5)
|
||||
record = log.logging_queue.get(block=True, timeout=5)
|
||||
json_record = self.json_record(record)
|
||||
socketio.emit(
|
||||
"log_entry", json_record,
|
||||
@@ -553,10 +559,10 @@ def setup_logging(config, flask_app, loglevel, quiet):
|
||||
flask_app.logger.disabled = True
|
||||
return
|
||||
|
||||
log_level = utils.LOG_LEVELS[loglevel]
|
||||
log_level = aprsd_config.LOG_LEVELS[loglevel]
|
||||
LOG.setLevel(log_level)
|
||||
log_format = config["aprsd"].get("logformat", utils.DEFAULT_LOG_FORMAT)
|
||||
date_format = config["aprsd"].get("dateformat", utils.DEFAULT_DATE_FORMAT)
|
||||
log_format = config["aprsd"].get("logformat", aprsd_config.DEFAULT_LOG_FORMAT)
|
||||
date_format = config["aprsd"].get("dateformat", aprsd_config.DEFAULT_DATE_FORMAT)
|
||||
log_formatter = logging.Formatter(fmt=log_format, datefmt=date_format)
|
||||
log_file = config["aprsd"].get("logfile", None)
|
||||
if log_file:
|
||||
|
||||
@@ -1,233 +0,0 @@
|
||||
#
|
||||
# Used to fetch the stats url and determine if
|
||||
# aprsd server is 'healthy'
|
||||
#
|
||||
#
|
||||
# python included libs
|
||||
import datetime
|
||||
import json
|
||||
import logging
|
||||
from logging import NullHandler
|
||||
from logging.handlers import RotatingFileHandler
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
|
||||
import click
|
||||
import click_completion
|
||||
import requests
|
||||
|
||||
# local imports here
|
||||
import aprsd
|
||||
from aprsd import utils
|
||||
|
||||
|
||||
# setup the global logger
|
||||
# logging.basicConfig(level=logging.DEBUG) # level=10
|
||||
LOG = logging.getLogger("APRSD")
|
||||
|
||||
LOG_LEVELS = {
|
||||
"CRITICAL": logging.CRITICAL,
|
||||
"ERROR": logging.ERROR,
|
||||
"WARNING": logging.WARNING,
|
||||
"INFO": logging.INFO,
|
||||
"DEBUG": logging.DEBUG,
|
||||
}
|
||||
|
||||
CONTEXT_SETTINGS = dict(help_option_names=["-h", "--help"])
|
||||
|
||||
|
||||
def custom_startswith(string, incomplete):
|
||||
"""A custom completion match that supports case insensitive matching."""
|
||||
if os.environ.get("_CLICK_COMPLETION_COMMAND_CASE_INSENSITIVE_COMPLETE"):
|
||||
string = string.lower()
|
||||
incomplete = incomplete.lower()
|
||||
return string.startswith(incomplete)
|
||||
|
||||
|
||||
click_completion.core.startswith = custom_startswith
|
||||
click_completion.init()
|
||||
|
||||
|
||||
cmd_help = """Shell completion for click-completion-command
|
||||
Available shell types:
|
||||
\b
|
||||
%s
|
||||
Default type: auto
|
||||
""" % "\n ".join(
|
||||
f"{k:<12} {click_completion.core.shells[k]}"
|
||||
for k in sorted(click_completion.core.shells.keys())
|
||||
)
|
||||
|
||||
|
||||
@click.group(help=cmd_help, context_settings=CONTEXT_SETTINGS)
|
||||
@click.version_option()
|
||||
def main():
|
||||
pass
|
||||
|
||||
|
||||
@main.command()
|
||||
@click.option(
|
||||
"-i",
|
||||
"--case-insensitive/--no-case-insensitive",
|
||||
help="Case insensitive completion",
|
||||
)
|
||||
@click.argument(
|
||||
"shell",
|
||||
required=False,
|
||||
type=click_completion.DocumentedChoice(click_completion.core.shells),
|
||||
)
|
||||
def show(shell, case_insensitive):
|
||||
"""Show the click-completion-command completion code"""
|
||||
extra_env = (
|
||||
{"_CLICK_COMPLETION_COMMAND_CASE_INSENSITIVE_COMPLETE": "ON"}
|
||||
if case_insensitive
|
||||
else {}
|
||||
)
|
||||
click.echo(click_completion.core.get_code(shell, extra_env=extra_env))
|
||||
|
||||
|
||||
@main.command()
|
||||
@click.option(
|
||||
"--append/--overwrite",
|
||||
help="Append the completion code to the file",
|
||||
default=None,
|
||||
)
|
||||
@click.option(
|
||||
"-i",
|
||||
"--case-insensitive/--no-case-insensitive",
|
||||
help="Case insensitive completion",
|
||||
)
|
||||
@click.argument(
|
||||
"shell",
|
||||
required=False,
|
||||
type=click_completion.DocumentedChoice(click_completion.core.shells),
|
||||
)
|
||||
@click.argument("path", required=False)
|
||||
def install(append, case_insensitive, shell, path):
|
||||
"""Install the click-completion-command completion"""
|
||||
extra_env = (
|
||||
{"_CLICK_COMPLETION_COMMAND_CASE_INSENSITIVE_COMPLETE": "ON"}
|
||||
if case_insensitive
|
||||
else {}
|
||||
)
|
||||
shell, path = click_completion.core.install(
|
||||
shell=shell,
|
||||
path=path,
|
||||
append=append,
|
||||
extra_env=extra_env,
|
||||
)
|
||||
click.echo(f"{shell} completion installed in {path}")
|
||||
|
||||
|
||||
# Setup the logging faciility
|
||||
# to disable logging to stdout, but still log to file
|
||||
# use the --quiet option on the cmdln
|
||||
def setup_logging(config, loglevel, quiet):
|
||||
log_level = LOG_LEVELS[loglevel]
|
||||
LOG.setLevel(log_level)
|
||||
log_format = "[%(asctime)s] [%(threadName)-12s] [%(levelname)-5.5s]" " %(message)s"
|
||||
date_format = "%m/%d/%Y %I:%M:%S %p"
|
||||
log_formatter = logging.Formatter(fmt=log_format, datefmt=date_format)
|
||||
log_file = config["aprs"].get("logfile", None)
|
||||
if log_file:
|
||||
fh = RotatingFileHandler(log_file, maxBytes=(10248576 * 5), backupCount=4)
|
||||
else:
|
||||
fh = NullHandler()
|
||||
|
||||
fh.setFormatter(log_formatter)
|
||||
LOG.addHandler(fh)
|
||||
|
||||
if not quiet:
|
||||
sh = logging.StreamHandler(sys.stdout)
|
||||
sh.setFormatter(log_formatter)
|
||||
LOG.addHandler(sh)
|
||||
|
||||
|
||||
def parse_delta_str(s):
|
||||
if "day" in s:
|
||||
m = re.match(
|
||||
r"(?P<days>[-\d]+) day[s]*, (?P<hours>\d+):(?P<minutes>\d+):(?P<seconds>\d[\.\d+]*)",
|
||||
s,
|
||||
)
|
||||
else:
|
||||
m = re.match(r"(?P<hours>\d+):(?P<minutes>\d+):(?P<seconds>\d[\.\d+]*)", s)
|
||||
return {key: float(val) for key, val in m.groupdict().items()}
|
||||
|
||||
|
||||
@main.command()
|
||||
@click.option(
|
||||
"--loglevel",
|
||||
default="INFO",
|
||||
show_default=True,
|
||||
type=click.Choice(
|
||||
["CRITICAL", "ERROR", "WARNING", "INFO", "DEBUG"],
|
||||
case_sensitive=False,
|
||||
),
|
||||
show_choices=True,
|
||||
help="The log level to use for aprsd.log",
|
||||
)
|
||||
@click.option(
|
||||
"-c",
|
||||
"--config",
|
||||
"config_file",
|
||||
show_default=True,
|
||||
default=utils.DEFAULT_CONFIG_FILE,
|
||||
help="The aprsd config file to use for options.",
|
||||
)
|
||||
@click.option(
|
||||
"--url",
|
||||
"health_url",
|
||||
show_default=True,
|
||||
default="http://localhost:8001/stats",
|
||||
help="The aprsd url to call for checking health/stats",
|
||||
)
|
||||
@click.option(
|
||||
"--timeout",
|
||||
show_default=True,
|
||||
default=3,
|
||||
help="How long to wait for healtcheck url to come back",
|
||||
)
|
||||
def check(loglevel, config_file, health_url, timeout):
|
||||
"""APRSD Plugin test app."""
|
||||
|
||||
config = utils.parse_config(config_file)
|
||||
|
||||
setup_logging(config, loglevel, False)
|
||||
LOG.debug(f"APRSD HealthCheck version: {aprsd.__version__}")
|
||||
|
||||
try:
|
||||
url = health_url
|
||||
response = requests.get(url, timeout=timeout)
|
||||
response.raise_for_status()
|
||||
except Exception as ex:
|
||||
LOG.error(f"Failed to fetch healthcheck url '{url}' : '{ex}'")
|
||||
sys.exit(-1)
|
||||
else:
|
||||
stats = json.loads(response.text)
|
||||
LOG.debug(stats)
|
||||
|
||||
email_thread_last_update = stats["stats"]["email"]["thread_last_update"]
|
||||
|
||||
delta = parse_delta_str(email_thread_last_update)
|
||||
d = datetime.timedelta(**delta)
|
||||
max_timeout = {"hours": 0.0, "minutes": 5, "seconds": 0}
|
||||
max_delta = datetime.timedelta(**max_timeout)
|
||||
if d > max_delta:
|
||||
LOG.error(f"Email thread is very old! {d}")
|
||||
sys.exit(-1)
|
||||
|
||||
aprsis_last_update = stats["stats"]["aprs-is"]["last_update"]
|
||||
delta = parse_delta_str(aprsis_last_update)
|
||||
d = datetime.timedelta(**delta)
|
||||
max_timeout = {"hours": 0.0, "minutes": 5, "seconds": 0}
|
||||
max_delta = datetime.timedelta(**max_timeout)
|
||||
if d > max_delta:
|
||||
LOG.error(f"APRS-IS last update is very old! {d}")
|
||||
sys.exit(-1)
|
||||
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
-392
@@ -1,392 +0,0 @@
|
||||
#
|
||||
# Listen on amateur radio aprs-is network for messages and respond to them.
|
||||
# You must have an amateur radio callsign to use this software. You must
|
||||
# create an ~/.aprsd/config.yml file with all of the required settings. To
|
||||
# generate an example config.yml, just run aprsd, then copy the sample config
|
||||
# to ~/.aprsd/config.yml and edit the settings.
|
||||
#
|
||||
# APRS messages:
|
||||
# l(ocation) = descriptive location of calling station
|
||||
# w(eather) = temp, (hi/low) forecast, later forecast
|
||||
# t(ime) = respond with the current time
|
||||
# f(ortune) = respond with a short fortune
|
||||
# -email_addr email text = send an email
|
||||
# -2 = display the last 2 emails received
|
||||
# p(ing) = respond with Pong!/time
|
||||
# anything else = respond with usage
|
||||
#
|
||||
# (C)2018 Craig Lamparter
|
||||
# License GPLv2
|
||||
#
|
||||
|
||||
# python included libs
|
||||
import datetime
|
||||
import logging
|
||||
from logging import NullHandler
|
||||
from logging.handlers import RotatingFileHandler
|
||||
import os
|
||||
import signal
|
||||
import sys
|
||||
import time
|
||||
|
||||
import aprslib
|
||||
from aprslib.exceptions import LoginError
|
||||
import click
|
||||
import click_completion
|
||||
|
||||
# local imports here
|
||||
import aprsd
|
||||
from aprsd import client, messaging, stats, threads, trace, utils
|
||||
|
||||
|
||||
# setup the global logger
|
||||
# logging.basicConfig(level=logging.DEBUG) # level=10
|
||||
LOG = logging.getLogger("APRSD")
|
||||
|
||||
|
||||
CONTEXT_SETTINGS = dict(help_option_names=["-h", "--help"])
|
||||
|
||||
flask_enabled = False
|
||||
|
||||
# server_event = threading.Event()
|
||||
|
||||
# localization, please edit:
|
||||
# HOST = "noam.aprs2.net" # north america tier2 servers round robin
|
||||
# USER = "KM6XXX-9" # callsign of this aprs client with SSID
|
||||
# PASS = "99999" # google how to generate this
|
||||
# BASECALLSIGN = "KM6XXX" # callsign of radio in the field to send email
|
||||
# shortcuts = {
|
||||
# "aa" : "5551239999@vtext.com",
|
||||
# "cl" : "craiglamparter@somedomain.org",
|
||||
# "wb" : "5553909472@vtext.com"
|
||||
# }
|
||||
|
||||
|
||||
def custom_startswith(string, incomplete):
|
||||
"""A custom completion match that supports case insensitive matching."""
|
||||
if os.environ.get("_CLICK_COMPLETION_COMMAND_CASE_INSENSITIVE_COMPLETE"):
|
||||
string = string.lower()
|
||||
incomplete = incomplete.lower()
|
||||
return string.startswith(incomplete)
|
||||
|
||||
|
||||
click_completion.core.startswith = custom_startswith
|
||||
click_completion.init()
|
||||
|
||||
|
||||
cmd_help = """Shell completion for click-completion-command
|
||||
Available shell types:
|
||||
\b
|
||||
%s
|
||||
Default type: auto
|
||||
""" % "\n ".join(
|
||||
f"{k:<12} {click_completion.core.shells[k]}"
|
||||
for k in sorted(click_completion.core.shells.keys())
|
||||
)
|
||||
|
||||
|
||||
@click.group(help=cmd_help, context_settings=CONTEXT_SETTINGS)
|
||||
@click.version_option()
|
||||
def main():
|
||||
pass
|
||||
|
||||
|
||||
@main.command()
|
||||
@click.option(
|
||||
"-i",
|
||||
"--case-insensitive/--no-case-insensitive",
|
||||
help="Case insensitive completion",
|
||||
)
|
||||
@click.argument(
|
||||
"shell",
|
||||
required=False,
|
||||
type=click_completion.DocumentedChoice(click_completion.core.shells),
|
||||
)
|
||||
def show(shell, case_insensitive):
|
||||
"""Show the click-completion-command completion code"""
|
||||
extra_env = (
|
||||
{"_CLICK_COMPLETION_COMMAND_CASE_INSENSITIVE_COMPLETE": "ON"}
|
||||
if case_insensitive
|
||||
else {}
|
||||
)
|
||||
click.echo(click_completion.core.get_code(shell, extra_env=extra_env))
|
||||
|
||||
|
||||
@main.command()
|
||||
@click.option(
|
||||
"--append/--overwrite",
|
||||
help="Append the completion code to the file",
|
||||
default=None,
|
||||
)
|
||||
@click.option(
|
||||
"-i",
|
||||
"--case-insensitive/--no-case-insensitive",
|
||||
help="Case insensitive completion",
|
||||
)
|
||||
@click.argument(
|
||||
"shell",
|
||||
required=False,
|
||||
type=click_completion.DocumentedChoice(click_completion.core.shells),
|
||||
)
|
||||
@click.argument("path", required=False)
|
||||
def install(append, case_insensitive, shell, path):
|
||||
"""Install the click-completion-command completion"""
|
||||
extra_env = (
|
||||
{"_CLICK_COMPLETION_COMMAND_CASE_INSENSITIVE_COMPLETE": "ON"}
|
||||
if case_insensitive
|
||||
else {}
|
||||
)
|
||||
shell, path = click_completion.core.install(
|
||||
shell=shell,
|
||||
path=path,
|
||||
append=append,
|
||||
extra_env=extra_env,
|
||||
)
|
||||
click.echo(f"{shell} completion installed in {path}")
|
||||
|
||||
|
||||
def signal_handler(sig, frame):
|
||||
global flask_enabled
|
||||
|
||||
threads.APRSDThreadList().stop_all()
|
||||
if "subprocess" not in str(frame):
|
||||
LOG.info(
|
||||
"Ctrl+C, Sending all threads exit! Can take up to 10 seconds {}".format(
|
||||
datetime.datetime.now(),
|
||||
),
|
||||
)
|
||||
time.sleep(5)
|
||||
tracker = messaging.MsgTrack()
|
||||
tracker.save()
|
||||
LOG.info(stats.APRSDStats())
|
||||
# signal.signal(signal.SIGTERM, sys.exit(0))
|
||||
# sys.exit(0)
|
||||
if flask_enabled:
|
||||
signal.signal(signal.SIGTERM, sys.exit(0))
|
||||
|
||||
|
||||
# Setup the logging faciility
|
||||
# to disable logging to stdout, but still log to file
|
||||
# use the --quiet option on the cmdln
|
||||
def setup_logging(config, loglevel, quiet):
|
||||
log_level = utils.LOG_LEVELS[loglevel]
|
||||
LOG.setLevel(log_level)
|
||||
log_format = config["aprsd"].get("logformat", utils.DEFAULT_LOG_FORMAT)
|
||||
date_format = config["aprsd"].get("dateformat", utils.DEFAULT_DATE_FORMAT)
|
||||
log_formatter = logging.Formatter(fmt=log_format, datefmt=date_format)
|
||||
log_file = config["aprsd"].get("logfile", None)
|
||||
if log_file:
|
||||
fh = RotatingFileHandler(log_file, maxBytes=(10248576 * 5), backupCount=4)
|
||||
else:
|
||||
fh = NullHandler()
|
||||
|
||||
fh.setFormatter(log_formatter)
|
||||
LOG.addHandler(fh)
|
||||
|
||||
imap_logger = None
|
||||
if config["aprsd"]["email"].get("enabled", False) and config["aprsd"]["email"][
|
||||
"imap"
|
||||
].get("debug", False):
|
||||
|
||||
imap_logger = logging.getLogger("imapclient.imaplib")
|
||||
imap_logger.setLevel(log_level)
|
||||
imap_logger.addHandler(fh)
|
||||
|
||||
if not quiet:
|
||||
sh = logging.StreamHandler(sys.stdout)
|
||||
sh.setFormatter(log_formatter)
|
||||
LOG.addHandler(sh)
|
||||
if imap_logger:
|
||||
imap_logger.addHandler(sh)
|
||||
|
||||
|
||||
@main.command()
|
||||
@click.option(
|
||||
"--loglevel",
|
||||
default="DEBUG",
|
||||
show_default=True,
|
||||
type=click.Choice(
|
||||
["CRITICAL", "ERROR", "WARNING", "INFO", "DEBUG"],
|
||||
case_sensitive=False,
|
||||
),
|
||||
show_choices=True,
|
||||
help="The log level to use for aprsd.log",
|
||||
)
|
||||
@click.option("--quiet", is_flag=True, default=False, help="Don't log to stdout")
|
||||
@click.option(
|
||||
"-c",
|
||||
"--config",
|
||||
"config_file",
|
||||
show_default=True,
|
||||
default=utils.DEFAULT_CONFIG_FILE,
|
||||
help="The aprsd config file to use for options.",
|
||||
)
|
||||
@click.option(
|
||||
"--aprs-login",
|
||||
envvar="APRS_LOGIN",
|
||||
show_envvar=True,
|
||||
help="What callsign to send the message from.",
|
||||
)
|
||||
@click.option(
|
||||
"--aprs-password",
|
||||
envvar="APRS_PASSWORD",
|
||||
show_envvar=True,
|
||||
help="the APRS-IS password for APRS_LOGIN",
|
||||
)
|
||||
@click.option(
|
||||
"--no-ack",
|
||||
"-n",
|
||||
is_flag=True,
|
||||
show_default=True,
|
||||
default=False,
|
||||
help="Don't wait for an ack, just sent it to APRS-IS and bail.",
|
||||
)
|
||||
@click.option("--raw", default=None, help="Send a raw message. Implies --no-ack")
|
||||
@click.argument("tocallsign", required=False)
|
||||
@click.argument("command", nargs=-1, required=False)
|
||||
def listen(
|
||||
loglevel,
|
||||
quiet,
|
||||
config_file,
|
||||
aprs_login,
|
||||
aprs_password,
|
||||
no_ack,
|
||||
raw,
|
||||
tocallsign,
|
||||
command,
|
||||
):
|
||||
"""Send a message to a callsign via APRS_IS."""
|
||||
global got_ack, got_response
|
||||
|
||||
config = utils.parse_config(config_file)
|
||||
if not aprs_login:
|
||||
click.echo("Must set --aprs_login or APRS_LOGIN")
|
||||
return
|
||||
|
||||
if not aprs_password:
|
||||
click.echo("Must set --aprs-password or APRS_PASSWORD")
|
||||
return
|
||||
|
||||
config["aprs"]["login"] = aprs_login
|
||||
config["aprs"]["password"] = aprs_password
|
||||
messaging.CONFIG = config
|
||||
|
||||
setup_logging(config, loglevel, quiet)
|
||||
LOG.info(f"APRSD TEST Started version: {aprsd.__version__}")
|
||||
if type(command) is tuple:
|
||||
command = " ".join(command)
|
||||
if not quiet:
|
||||
if raw:
|
||||
LOG.info(f"L'{aprs_login}' R'{raw}'")
|
||||
else:
|
||||
LOG.info(f"L'{aprs_login}' To'{tocallsign}' C'{command}'")
|
||||
|
||||
flat_config = utils.flatten_dict(config)
|
||||
LOG.info("Using CONFIG values:")
|
||||
for x in flat_config:
|
||||
if "password" in x or "aprsd.web.users.admin" in x:
|
||||
LOG.info(f"{x} = XXXXXXXXXXXXXXXXXXX")
|
||||
else:
|
||||
LOG.info(f"{x} = {flat_config[x]}")
|
||||
|
||||
got_ack = False
|
||||
got_response = False
|
||||
|
||||
# TODO(walt) - manually edit this list
|
||||
# prior to running aprsd-listen listen
|
||||
watch_list = ["k*"]
|
||||
|
||||
# build last seen list
|
||||
last_seen = {}
|
||||
for callsign in watch_list:
|
||||
call = callsign.replace("*", "")
|
||||
last_seen[call] = datetime.datetime.now()
|
||||
|
||||
LOG.debug("Last seen list")
|
||||
LOG.debug(last_seen)
|
||||
|
||||
@trace.trace
|
||||
def rx_packet(packet):
|
||||
global got_ack, got_response
|
||||
LOG.debug("Got packet back {}".format(packet["raw"]))
|
||||
|
||||
if packet["from"] in last_seen:
|
||||
now = datetime.datetime.now()
|
||||
age = str(now - last_seen[packet["from"]])
|
||||
|
||||
delta = utils.parse_delta_str(age)
|
||||
d = datetime.timedelta(**delta)
|
||||
|
||||
max_timeout = {
|
||||
"seconds": config["aprsd"]["watch_list"]["alert_time_seconds"],
|
||||
}
|
||||
max_delta = datetime.timedelta(**max_timeout)
|
||||
if d > max_delta:
|
||||
LOG.debug(
|
||||
"NOTIFY!!! {} last seen {} max age={}".format(
|
||||
packet["from"],
|
||||
age,
|
||||
max_delta,
|
||||
),
|
||||
)
|
||||
else:
|
||||
LOG.debug(f"Not old enough to notify {d} < {max_delta}")
|
||||
LOG.debug("Update last seen from {}".format(packet["from"]))
|
||||
last_seen[packet["from"]] = now
|
||||
else:
|
||||
LOG.debug(
|
||||
"ignoring packet because {} not in watch list".format(packet["from"]),
|
||||
)
|
||||
|
||||
resp = packet.get("response", None)
|
||||
if resp == "ack":
|
||||
ack_num = packet.get("msgNo")
|
||||
LOG.info(f"We saw an ACK {ack_num} Ignoring")
|
||||
# messaging.log_packet(packet)
|
||||
got_ack = True
|
||||
else:
|
||||
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,
|
||||
)
|
||||
|
||||
try:
|
||||
cl = client.Client(config)
|
||||
cl.setup_connection()
|
||||
except LoginError:
|
||||
sys.exit(-1)
|
||||
|
||||
aprs_client = client.get_client()
|
||||
|
||||
# filter_str = 'b/{}'.format('/'.join(watch_list))
|
||||
# LOG.debug("Filter by '{}'".format(filter_str))
|
||||
# aprs_client.set_filter(filter_str)
|
||||
filter_str = "p/{}".format("/".join(watch_list))
|
||||
LOG.debug(f"Filter by '{filter_str}'")
|
||||
aprs_client.set_filter(filter_str)
|
||||
|
||||
while True:
|
||||
try:
|
||||
# This will register a packet consumer with aprslib
|
||||
# When new packets come in the consumer will process
|
||||
# the packet
|
||||
aprs_client.consumer(rx_packet, raw=False)
|
||||
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
|
||||
cl.reset()
|
||||
except aprslib.exceptions.UnknownFormat:
|
||||
LOG.error("Got a shitty packet")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,70 @@
|
||||
import logging
|
||||
from logging import NullHandler
|
||||
from logging.handlers import RotatingFileHandler
|
||||
import queue
|
||||
import sys
|
||||
|
||||
from aprsd import config as aprsd_config
|
||||
|
||||
|
||||
LOG = logging.getLogger("APRSD")
|
||||
logging_queue = queue.Queue()
|
||||
|
||||
|
||||
# Setup the logging faciility
|
||||
# to disable logging to stdout, but still log to file
|
||||
# use the --quiet option on the cmdln
|
||||
def setup_logging(config, loglevel, quiet):
|
||||
log_level = aprsd_config.LOG_LEVELS[loglevel]
|
||||
LOG.setLevel(log_level)
|
||||
log_format = config["aprsd"].get("logformat", aprsd_config.DEFAULT_LOG_FORMAT)
|
||||
date_format = config["aprsd"].get("dateformat", aprsd_config.DEFAULT_DATE_FORMAT)
|
||||
log_formatter = logging.Formatter(fmt=log_format, datefmt=date_format)
|
||||
log_file = config["aprsd"].get("logfile", None)
|
||||
if log_file:
|
||||
fh = RotatingFileHandler(log_file, maxBytes=(10248576 * 5), backupCount=4)
|
||||
else:
|
||||
fh = NullHandler()
|
||||
|
||||
fh.setFormatter(log_formatter)
|
||||
LOG.addHandler(fh)
|
||||
|
||||
imap_logger = None
|
||||
if config.get("aprsd.email.enabled", default=False) and config.get("aprsd.email.imap.debug", default=False):
|
||||
|
||||
imap_logger = logging.getLogger("imapclient.imaplib")
|
||||
imap_logger.setLevel(log_level)
|
||||
imap_logger.addHandler(fh)
|
||||
|
||||
if config.get("aprsd.web.enabled", default=False):
|
||||
qh = logging.handlers.QueueHandler(logging_queue)
|
||||
q_log_formatter = logging.Formatter(
|
||||
fmt=aprsd_config.QUEUE_LOG_FORMAT,
|
||||
datefmt=aprsd_config.QUEUE_DATE_FORMAT,
|
||||
)
|
||||
qh.setFormatter(q_log_formatter)
|
||||
LOG.addHandler(qh)
|
||||
|
||||
if not quiet:
|
||||
sh = logging.StreamHandler(sys.stdout)
|
||||
sh.setFormatter(log_formatter)
|
||||
LOG.addHandler(sh)
|
||||
if imap_logger:
|
||||
imap_logger.addHandler(sh)
|
||||
|
||||
|
||||
def setup_logging_no_config(loglevel, quiet):
|
||||
log_level = aprsd_config.LOG_LEVELS[loglevel]
|
||||
LOG.setLevel(log_level)
|
||||
log_format = aprsd_config.DEFAULT_LOG_FORMAT
|
||||
date_format = aprsd_config.DEFAULT_DATE_FORMAT
|
||||
log_formatter = logging.Formatter(fmt=log_format, datefmt=date_format)
|
||||
fh = NullHandler()
|
||||
|
||||
fh.setFormatter(log_formatter)
|
||||
LOG.addHandler(fh)
|
||||
|
||||
if not quiet:
|
||||
sh = logging.StreamHandler(sys.stdout)
|
||||
sh.setFormatter(log_formatter)
|
||||
LOG.addHandler(sh)
|
||||
-543
@@ -1,543 +0,0 @@
|
||||
#
|
||||
# Listen on amateur radio aprs-is network for messages and respond to them.
|
||||
# You must have an amateur radio callsign to use this software. You must
|
||||
# create an ~/.aprsd/config.yml file with all of the required settings. To
|
||||
# generate an example config.yml, just run aprsd, then copy the sample config
|
||||
# to ~/.aprsd/config.yml and edit the settings.
|
||||
#
|
||||
# APRS messages:
|
||||
# l(ocation) = descriptive location of calling station
|
||||
# w(eather) = temp, (hi/low) forecast, later forecast
|
||||
# t(ime) = respond with the current time
|
||||
# f(ortune) = respond with a short fortune
|
||||
# -email_addr email text = send an email
|
||||
# -2 = display the last 2 emails received
|
||||
# p(ing) = respond with Pong!/time
|
||||
# anything else = respond with usage
|
||||
#
|
||||
# (C)2018 Craig Lamparter
|
||||
# License GPLv2
|
||||
#
|
||||
|
||||
# python included libs
|
||||
import datetime
|
||||
import logging
|
||||
from logging import NullHandler
|
||||
from logging.handlers import RotatingFileHandler
|
||||
import os
|
||||
import signal
|
||||
import sys
|
||||
import time
|
||||
|
||||
import aprslib
|
||||
from aprslib.exceptions import LoginError
|
||||
import click
|
||||
import click_completion
|
||||
|
||||
# local imports here
|
||||
import aprsd
|
||||
from aprsd import (
|
||||
client, flask, kissclient, messaging, packets, plugin, stats, threads,
|
||||
trace, utils,
|
||||
)
|
||||
|
||||
|
||||
# setup the global logger
|
||||
# logging.basicConfig(level=logging.DEBUG) # level=10
|
||||
LOG = logging.getLogger("APRSD")
|
||||
|
||||
|
||||
CONTEXT_SETTINGS = dict(help_option_names=["-h", "--help"])
|
||||
|
||||
flask_enabled = False
|
||||
|
||||
# server_event = threading.Event()
|
||||
|
||||
# localization, please edit:
|
||||
# HOST = "noam.aprs2.net" # north america tier2 servers round robin
|
||||
# USER = "KM6XXX-9" # callsign of this aprs client with SSID
|
||||
# PASS = "99999" # google how to generate this
|
||||
# BASECALLSIGN = "KM6XXX" # callsign of radio in the field to send email
|
||||
# shortcuts = {
|
||||
# "aa" : "5551239999@vtext.com",
|
||||
# "cl" : "craiglamparter@somedomain.org",
|
||||
# "wb" : "5553909472@vtext.com"
|
||||
# }
|
||||
|
||||
|
||||
def custom_startswith(string, incomplete):
|
||||
"""A custom completion match that supports case insensitive matching."""
|
||||
if os.environ.get("_CLICK_COMPLETION_COMMAND_CASE_INSENSITIVE_COMPLETE"):
|
||||
string = string.lower()
|
||||
incomplete = incomplete.lower()
|
||||
return string.startswith(incomplete)
|
||||
|
||||
|
||||
click_completion.core.startswith = custom_startswith
|
||||
click_completion.init()
|
||||
|
||||
|
||||
cmd_help = """Shell completion for click-completion-command
|
||||
Available shell types:
|
||||
\b
|
||||
%s
|
||||
Default type: auto
|
||||
""" % "\n ".join(
|
||||
f"{k:<12} {click_completion.core.shells[k]}"
|
||||
for k in sorted(click_completion.core.shells.keys())
|
||||
)
|
||||
|
||||
|
||||
@click.group(help=cmd_help, context_settings=CONTEXT_SETTINGS)
|
||||
@click.version_option()
|
||||
def main():
|
||||
pass
|
||||
|
||||
|
||||
@main.command()
|
||||
@click.option(
|
||||
"-i",
|
||||
"--case-insensitive/--no-case-insensitive",
|
||||
help="Case insensitive completion",
|
||||
)
|
||||
@click.argument(
|
||||
"shell",
|
||||
required=False,
|
||||
type=click_completion.DocumentedChoice(click_completion.core.shells),
|
||||
)
|
||||
def show(shell, case_insensitive):
|
||||
"""Show the click-completion-command completion code"""
|
||||
extra_env = (
|
||||
{"_CLICK_COMPLETION_COMMAND_CASE_INSENSITIVE_COMPLETE": "ON"}
|
||||
if case_insensitive
|
||||
else {}
|
||||
)
|
||||
click.echo(click_completion.core.get_code(shell, extra_env=extra_env))
|
||||
|
||||
|
||||
@main.command()
|
||||
@click.option(
|
||||
"--append/--overwrite",
|
||||
help="Append the completion code to the file",
|
||||
default=None,
|
||||
)
|
||||
@click.option(
|
||||
"-i",
|
||||
"--case-insensitive/--no-case-insensitive",
|
||||
help="Case insensitive completion",
|
||||
)
|
||||
@click.argument(
|
||||
"shell",
|
||||
required=False,
|
||||
type=click_completion.DocumentedChoice(click_completion.core.shells),
|
||||
)
|
||||
@click.argument("path", required=False)
|
||||
def install(append, case_insensitive, shell, path):
|
||||
"""Install the click-completion-command completion"""
|
||||
extra_env = (
|
||||
{"_CLICK_COMPLETION_COMMAND_CASE_INSENSITIVE_COMPLETE": "ON"}
|
||||
if case_insensitive
|
||||
else {}
|
||||
)
|
||||
shell, path = click_completion.core.install(
|
||||
shell=shell,
|
||||
path=path,
|
||||
append=append,
|
||||
extra_env=extra_env,
|
||||
)
|
||||
click.echo(f"{shell} completion installed in {path}")
|
||||
|
||||
|
||||
def signal_handler(sig, frame):
|
||||
global flask_enabled
|
||||
|
||||
threads.APRSDThreadList().stop_all()
|
||||
if "subprocess" not in str(frame):
|
||||
LOG.info(
|
||||
"Ctrl+C, Sending all threads exit! Can take up to 10 seconds {}".format(
|
||||
datetime.datetime.now(),
|
||||
),
|
||||
)
|
||||
time.sleep(1.5)
|
||||
tracker = messaging.MsgTrack()
|
||||
tracker.save()
|
||||
LOG.info(stats.APRSDStats())
|
||||
# signal.signal(signal.SIGTERM, sys.exit(0))
|
||||
# sys.exit(0)
|
||||
if flask_enabled:
|
||||
signal.signal(signal.SIGTERM, sys.exit(0))
|
||||
|
||||
|
||||
# Setup the logging faciility
|
||||
# to disable logging to stdout, but still log to file
|
||||
# use the --quiet option on the cmdln
|
||||
def setup_logging(config, loglevel, quiet):
|
||||
log_level = utils.LOG_LEVELS[loglevel]
|
||||
LOG.setLevel(log_level)
|
||||
log_format = config["aprsd"].get("logformat", utils.DEFAULT_LOG_FORMAT)
|
||||
date_format = config["aprsd"].get("dateformat", utils.DEFAULT_DATE_FORMAT)
|
||||
log_formatter = logging.Formatter(fmt=log_format, datefmt=date_format)
|
||||
log_file = config["aprsd"].get("logfile", None)
|
||||
if log_file:
|
||||
fh = RotatingFileHandler(log_file, maxBytes=(10248576 * 5), backupCount=4)
|
||||
else:
|
||||
fh = NullHandler()
|
||||
|
||||
fh.setFormatter(log_formatter)
|
||||
LOG.addHandler(fh)
|
||||
|
||||
imap_logger = None
|
||||
if config["aprsd"]["email"].get("enabled", False) and config["aprsd"]["email"][
|
||||
"imap"
|
||||
].get("debug", False):
|
||||
|
||||
imap_logger = logging.getLogger("imapclient.imaplib")
|
||||
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)
|
||||
LOG.addHandler(sh)
|
||||
if imap_logger:
|
||||
imap_logger.addHandler(sh)
|
||||
|
||||
|
||||
@main.command()
|
||||
@click.option(
|
||||
"--loglevel",
|
||||
default="INFO",
|
||||
show_default=True,
|
||||
type=click.Choice(
|
||||
["CRITICAL", "ERROR", "WARNING", "INFO", "DEBUG"],
|
||||
case_sensitive=False,
|
||||
),
|
||||
show_choices=True,
|
||||
help="The log level to use for aprsd.log",
|
||||
)
|
||||
@click.option(
|
||||
"-c",
|
||||
"--config",
|
||||
"config_file",
|
||||
show_default=True,
|
||||
default=utils.DEFAULT_CONFIG_FILE,
|
||||
help="The aprsd config file to use for options.",
|
||||
)
|
||||
def check_version(loglevel, config_file):
|
||||
config = utils.parse_config(config_file)
|
||||
|
||||
setup_logging(config, loglevel, False)
|
||||
level, msg = utils._check_version()
|
||||
if level:
|
||||
LOG.warning(msg)
|
||||
else:
|
||||
LOG.info(msg)
|
||||
|
||||
|
||||
@main.command()
|
||||
def sample_config():
|
||||
"""This dumps the config to stdout."""
|
||||
click.echo(utils.dump_default_cfg())
|
||||
|
||||
|
||||
@main.command()
|
||||
@click.option(
|
||||
"--loglevel",
|
||||
default="DEBUG",
|
||||
show_default=True,
|
||||
type=click.Choice(
|
||||
["CRITICAL", "ERROR", "WARNING", "INFO", "DEBUG"],
|
||||
case_sensitive=False,
|
||||
),
|
||||
show_choices=True,
|
||||
help="The log level to use for aprsd.log",
|
||||
)
|
||||
@click.option("--quiet", is_flag=True, default=False, help="Don't log to stdout")
|
||||
@click.option(
|
||||
"-c",
|
||||
"--config",
|
||||
"config_file",
|
||||
show_default=True,
|
||||
default=utils.DEFAULT_CONFIG_FILE,
|
||||
help="The aprsd config file to use for options.",
|
||||
)
|
||||
@click.option(
|
||||
"--aprs-login",
|
||||
envvar="APRS_LOGIN",
|
||||
show_envvar=True,
|
||||
help="What callsign to send the message from.",
|
||||
)
|
||||
@click.option(
|
||||
"--aprs-password",
|
||||
envvar="APRS_PASSWORD",
|
||||
show_envvar=True,
|
||||
help="the APRS-IS password for APRS_LOGIN",
|
||||
)
|
||||
@click.option(
|
||||
"--no-ack",
|
||||
"-n",
|
||||
is_flag=True,
|
||||
show_default=True,
|
||||
default=False,
|
||||
help="Don't wait for an ack, just sent it to APRS-IS and bail.",
|
||||
)
|
||||
@click.option("--raw", default=None, help="Send a raw message. Implies --no-ack")
|
||||
@click.argument("tocallsign", required=False)
|
||||
@click.argument("command", nargs=-1, required=False)
|
||||
def send_message(
|
||||
loglevel,
|
||||
quiet,
|
||||
config_file,
|
||||
aprs_login,
|
||||
aprs_password,
|
||||
no_ack,
|
||||
raw,
|
||||
tocallsign,
|
||||
command,
|
||||
):
|
||||
"""Send a message to a callsign via APRS_IS."""
|
||||
global got_ack, got_response
|
||||
|
||||
config = utils.parse_config(config_file)
|
||||
if not aprs_login:
|
||||
click.echo("Must set --aprs_login or APRS_LOGIN")
|
||||
return
|
||||
|
||||
if not aprs_password:
|
||||
click.echo("Must set --aprs-password or APRS_PASSWORD")
|
||||
return
|
||||
|
||||
config["aprs"]["login"] = aprs_login
|
||||
config["aprs"]["password"] = aprs_password
|
||||
messaging.CONFIG = config
|
||||
|
||||
setup_logging(config, loglevel, quiet)
|
||||
LOG.info(f"APRSD Started version: {aprsd.__version__}")
|
||||
if type(command) is tuple:
|
||||
command = " ".join(command)
|
||||
if not quiet:
|
||||
if raw:
|
||||
LOG.info(f"L'{aprs_login}' R'{raw}'")
|
||||
else:
|
||||
LOG.info(f"L'{aprs_login}' To'{tocallsign}' C'{command}'")
|
||||
|
||||
got_ack = False
|
||||
got_response = False
|
||||
|
||||
def rx_packet(packet):
|
||||
global got_ack, got_response
|
||||
# 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)
|
||||
got_ack = True
|
||||
else:
|
||||
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,
|
||||
)
|
||||
got_response = True
|
||||
# Send the ack back?
|
||||
ack = messaging.AckMessage(
|
||||
config["aprs"]["login"],
|
||||
fromcall,
|
||||
msg_id=msg_number,
|
||||
)
|
||||
ack.send_direct()
|
||||
|
||||
if got_ack and got_response:
|
||||
sys.exit(0)
|
||||
|
||||
try:
|
||||
cl = client.Client(config)
|
||||
cl.setup_connection()
|
||||
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
|
||||
# we should bail after we get the ack and send an ack back for the
|
||||
# message
|
||||
if raw:
|
||||
msg = messaging.RawMessage(raw)
|
||||
msg.send_direct()
|
||||
sys.exit(0)
|
||||
else:
|
||||
msg = messaging.TextMessage(aprs_login, tocallsign, command)
|
||||
msg.send_direct()
|
||||
|
||||
if no_ack:
|
||||
sys.exit(0)
|
||||
|
||||
try:
|
||||
# This will register a packet consumer with aprslib
|
||||
# When new packets come in the consumer will process
|
||||
# the packet
|
||||
aprs_client = client.get_client()
|
||||
aprs_client.consumer(rx_packet, raw=False)
|
||||
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
|
||||
cl.reset()
|
||||
|
||||
|
||||
# main() ###
|
||||
@main.command()
|
||||
@click.option(
|
||||
"--loglevel",
|
||||
default="INFO",
|
||||
show_default=True,
|
||||
type=click.Choice(
|
||||
["CRITICAL", "ERROR", "WARNING", "INFO", "DEBUG"],
|
||||
case_sensitive=False,
|
||||
),
|
||||
show_choices=True,
|
||||
help="The log level to use for aprsd.log",
|
||||
)
|
||||
@click.option("--quiet", is_flag=True, default=False, help="Don't log to stdout")
|
||||
@click.option(
|
||||
"-c",
|
||||
"--config",
|
||||
"config_file",
|
||||
show_default=True,
|
||||
default=utils.DEFAULT_CONFIG_FILE,
|
||||
help="The aprsd config file to use for options.",
|
||||
)
|
||||
@click.option(
|
||||
"-f",
|
||||
"--flush",
|
||||
"flush",
|
||||
is_flag=True,
|
||||
show_default=True,
|
||||
default=False,
|
||||
help="Flush out all old aged messages on disk.",
|
||||
)
|
||||
def server(
|
||||
loglevel,
|
||||
quiet,
|
||||
config_file,
|
||||
flush,
|
||||
):
|
||||
"""Start the aprsd server process."""
|
||||
global flask_enabled
|
||||
signal.signal(signal.SIGINT, signal_handler)
|
||||
|
||||
if not quiet:
|
||||
click.echo("Load config")
|
||||
|
||||
config = utils.parse_config(config_file)
|
||||
|
||||
setup_logging(config, loglevel, quiet)
|
||||
level, msg = utils._check_version()
|
||||
if level:
|
||||
LOG.warning(msg)
|
||||
else:
|
||||
LOG.info(msg)
|
||||
LOG.info(f"APRSD Started version: {aprsd.__version__}")
|
||||
|
||||
flat_config = utils.flatten_dict(config)
|
||||
LOG.info("Using CONFIG values:")
|
||||
for x in flat_config:
|
||||
if "password" in x or "aprsd.web.users.admin" in x:
|
||||
LOG.info(f"{x} = XXXXXXXXXXXXXXXXXXX")
|
||||
else:
|
||||
LOG.info(f"{x} = {flat_config[x]}")
|
||||
|
||||
if config["aprsd"].get("trace", False):
|
||||
trace.setup_tracing(["method", "api"])
|
||||
stats.APRSDStats(config)
|
||||
|
||||
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)
|
||||
plugin_manager.setup_plugins()
|
||||
|
||||
# Now load the msgTrack from disk if any
|
||||
if flush:
|
||||
LOG.debug("Deleting saved MsgTrack.")
|
||||
messaging.MsgTrack().flush()
|
||||
else:
|
||||
# Try and load saved MsgTrack list
|
||||
LOG.debug("Loading saved MsgTrack object.")
|
||||
messaging.MsgTrack().load()
|
||||
|
||||
packets.PacketList(config=config)
|
||||
packets.WatchList(config=config)
|
||||
|
||||
if kissclient.KISSClient.kiss_enabled(config):
|
||||
kcl = kissclient.KISSClient(config=config)
|
||||
# This initializes the client object.
|
||||
kcl.client
|
||||
|
||||
kissrx_thread = threads.KISSRXThread(msg_queues=threads.msg_queues, config=config)
|
||||
kissrx_thread.start()
|
||||
|
||||
messaging.MsgTrack().restart()
|
||||
|
||||
keepalive = threads.KeepAliveThread(config=config)
|
||||
keepalive.start()
|
||||
|
||||
web_enabled = utils.check_config_option(config, ["aprsd", "web", "enabled"], default_fail=False)
|
||||
|
||||
if web_enabled:
|
||||
flask_enabled = True
|
||||
(socketio, app) = flask.init_flask(config, loglevel, quiet)
|
||||
socketio.run(
|
||||
app,
|
||||
host=config["aprsd"]["web"]["host"],
|
||||
port=config["aprsd"]["web"]["port"],
|
||||
)
|
||||
|
||||
# If there are items in the msgTracker, then save them
|
||||
LOG.info("APRSD Exiting.")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
+34
-94
@@ -2,14 +2,11 @@ import abc
|
||||
import datetime
|
||||
import logging
|
||||
from multiprocessing import RawValue
|
||||
import os
|
||||
import pathlib
|
||||
import pickle
|
||||
import re
|
||||
import threading
|
||||
import time
|
||||
|
||||
from aprsd import client, kissclient, packets, stats, threads, trace, utils
|
||||
from aprsd import client, objectstore, packets, stats, threads
|
||||
|
||||
|
||||
LOG = logging.getLogger("APRSD")
|
||||
@@ -18,12 +15,8 @@ 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 MsgTrack(objectstore.ObjectStoreMixin):
|
||||
"""Class to keep track of outstanding text messages.
|
||||
|
||||
This is a thread safe class that keeps track of active
|
||||
@@ -34,20 +27,13 @@ class MsgTrack:
|
||||
automatically adds itself to this class. When the ack is
|
||||
recieved from the radio, the message object is removed from
|
||||
this class.
|
||||
|
||||
# TODO(hemna)
|
||||
When aprsd is asked to quit this class should be serialized and
|
||||
saved to disk/db to keep track of the state of outstanding messages.
|
||||
When aprsd is started, it should try and fetch the saved state,
|
||||
and reloaded to a live state.
|
||||
|
||||
"""
|
||||
|
||||
_instance = None
|
||||
_start_time = None
|
||||
lock = None
|
||||
|
||||
track = {}
|
||||
data = {}
|
||||
total_messages_tracked = 0
|
||||
|
||||
def __new__(cls, *args, **kwargs):
|
||||
@@ -56,93 +42,65 @@ class MsgTrack:
|
||||
cls._instance.track = {}
|
||||
cls._instance._start_time = datetime.datetime.now()
|
||||
cls._instance.lock = threading.Lock()
|
||||
cls._instance.config = kwargs["config"]
|
||||
cls._instance._init_store()
|
||||
return cls._instance
|
||||
|
||||
def __getitem__(self, name):
|
||||
with self.lock:
|
||||
return self.track[name]
|
||||
return self.data[name]
|
||||
|
||||
def __iter__(self):
|
||||
with self.lock:
|
||||
return iter(self.track)
|
||||
return iter(self.data)
|
||||
|
||||
def keys(self):
|
||||
with self.lock:
|
||||
return self.track.keys()
|
||||
return self.data.keys()
|
||||
|
||||
def items(self):
|
||||
with self.lock:
|
||||
return self.track.items()
|
||||
return self.data.items()
|
||||
|
||||
def values(self):
|
||||
with self.lock:
|
||||
return self.track.values()
|
||||
return self.data.values()
|
||||
|
||||
def __len__(self):
|
||||
with self.lock:
|
||||
return len(self.track)
|
||||
return len(self.data)
|
||||
|
||||
def __str__(self):
|
||||
with self.lock:
|
||||
result = "{"
|
||||
for key in self.track.keys():
|
||||
result += f"{key}: {str(self.track[key])}, "
|
||||
for key in self.data.keys():
|
||||
result += f"{key}: {str(self.data[key])}, "
|
||||
result += "}"
|
||||
return result
|
||||
|
||||
def add(self, msg):
|
||||
with self.lock:
|
||||
key = int(msg.id)
|
||||
self.track[key] = msg
|
||||
self.data[key] = msg
|
||||
stats.APRSDStats().msgs_tracked_inc()
|
||||
self.total_messages_tracked += 1
|
||||
|
||||
def get(self, id):
|
||||
with self.lock:
|
||||
if id in self.track:
|
||||
return self.track[id]
|
||||
if id in self.data:
|
||||
return self.data[id]
|
||||
|
||||
def remove(self, id):
|
||||
with self.lock:
|
||||
key = int(id)
|
||||
if key in self.track.keys():
|
||||
del self.track[key]
|
||||
|
||||
def save(self):
|
||||
"""Save any queued to disk?"""
|
||||
LOG.debug(f"Save tracker to disk? {len(self)}")
|
||||
if len(self) > 0:
|
||||
LOG.info(f"Saving {len(self)} tracking messages to disk")
|
||||
pickle.dump(self.dump(), open(utils.DEFAULT_SAVE_FILE, "wb+"))
|
||||
else:
|
||||
LOG.debug(
|
||||
"Nothing to save, flushing old save file '{}'".format(
|
||||
utils.DEFAULT_SAVE_FILE,
|
||||
),
|
||||
)
|
||||
self.flush()
|
||||
|
||||
def dump(self):
|
||||
dump = {}
|
||||
with self.lock:
|
||||
for key in self.track.keys():
|
||||
dump[key] = self.track[key]
|
||||
|
||||
return dump
|
||||
|
||||
def load(self):
|
||||
if os.path.exists(utils.DEFAULT_SAVE_FILE):
|
||||
raw = pickle.load(open(utils.DEFAULT_SAVE_FILE, "rb"))
|
||||
if raw:
|
||||
self.track = raw
|
||||
LOG.debug("Loaded MsgTrack dict from disk.")
|
||||
LOG.debug(self)
|
||||
if key in self.data.keys():
|
||||
del self.data[key]
|
||||
|
||||
def restart(self):
|
||||
"""Walk the list of messages and restart them if any."""
|
||||
|
||||
for key in self.track.keys():
|
||||
msg = self.track[key]
|
||||
for key in self.data.keys():
|
||||
msg = self.data[key]
|
||||
if msg.last_send_attempt < msg.retry_count:
|
||||
msg.send()
|
||||
|
||||
@@ -154,14 +112,14 @@ class MsgTrack:
|
||||
"""Walk the list of delayed messages and restart them if any."""
|
||||
if not count:
|
||||
# Send all the delayed messages
|
||||
for key in self.track.keys():
|
||||
msg = self.track[key]
|
||||
for key in self.data.keys():
|
||||
msg = self.data[key]
|
||||
if msg.last_send_attempt == msg.retry_count:
|
||||
self._resend(msg)
|
||||
else:
|
||||
# They want to resend <count> delayed messages
|
||||
tmp = sorted(
|
||||
self.track.items(),
|
||||
self.data.items(),
|
||||
reverse=most_recent,
|
||||
key=lambda x: x[1].last_send_time,
|
||||
)
|
||||
@@ -169,13 +127,6 @@ class MsgTrack:
|
||||
for (_key, msg) in msg_list:
|
||||
self._resend(msg)
|
||||
|
||||
def flush(self):
|
||||
"""Nuke the old pickle file that stored the old results from last aprsd run."""
|
||||
if os.path.exists(utils.DEFAULT_SAVE_FILE):
|
||||
pathlib.Path(utils.DEFAULT_SAVE_FILE).unlink()
|
||||
with self.lock:
|
||||
self.track = {}
|
||||
|
||||
|
||||
class MessageCounter:
|
||||
"""
|
||||
@@ -239,7 +190,6 @@ class Message(metaclass=abc.ABCMeta):
|
||||
fromcall,
|
||||
tocall,
|
||||
msg_id=None,
|
||||
transport=MESSAGE_TRANSPORT_APRSIS,
|
||||
):
|
||||
self.fromcall = fromcall
|
||||
self.tocall = tocall
|
||||
@@ -248,18 +198,11 @@ 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.
|
||||
@@ -271,8 +214,8 @@ class RawMessage(Message):
|
||||
|
||||
message = None
|
||||
|
||||
def __init__(self, message, transport=MESSAGE_TRANSPORT_APRSIS):
|
||||
super().__init__(None, None, msg_id=None, transport=transport)
|
||||
def __init__(self, message):
|
||||
super().__init__(None, None, msg_id=None)
|
||||
self.message = message
|
||||
|
||||
def dict(self):
|
||||
@@ -301,7 +244,7 @@ class RawMessage(Message):
|
||||
|
||||
def send_direct(self, aprsis_client=None):
|
||||
"""Send a message without a separate thread."""
|
||||
cl = self.get_transport()
|
||||
cl = client.factory.create().client
|
||||
log_message(
|
||||
"Sending Message Direct",
|
||||
str(self).rstrip("\n"),
|
||||
@@ -310,7 +253,7 @@ class RawMessage(Message):
|
||||
fromcall=self.fromcall,
|
||||
)
|
||||
cl.send(self)
|
||||
stats.APRSDStats().msgs_sent_inc()
|
||||
stats.APRSDStats().msgs_tx_inc()
|
||||
|
||||
|
||||
class TextMessage(Message):
|
||||
@@ -325,9 +268,8 @@ class TextMessage(Message):
|
||||
message,
|
||||
msg_id=None,
|
||||
allow_delay=True,
|
||||
transport=MESSAGE_TRANSPORT_APRSIS,
|
||||
):
|
||||
super().__init__(fromcall, tocall, msg_id, transport=transport)
|
||||
super().__init__(fromcall, tocall, msg_id)
|
||||
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.
|
||||
@@ -384,7 +326,7 @@ class TextMessage(Message):
|
||||
if aprsis_client:
|
||||
cl = aprsis_client
|
||||
else:
|
||||
cl = self.get_transport()
|
||||
cl = client.factory.create().client
|
||||
log_message(
|
||||
"Sending Message Direct",
|
||||
str(self).rstrip("\n"),
|
||||
@@ -422,7 +364,6 @@ 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
|
||||
@@ -453,6 +394,7 @@ class SendMessageThread(threads.APRSDThread):
|
||||
retry_number=msg.last_send_attempt,
|
||||
msg_num=msg.id,
|
||||
)
|
||||
cl = client.factory.create().client
|
||||
cl.send(msg)
|
||||
stats.APRSDStats().msgs_tx_inc()
|
||||
packets.PacketList().add(msg.dict())
|
||||
@@ -467,8 +409,8 @@ class SendMessageThread(threads.APRSDThread):
|
||||
class AckMessage(Message):
|
||||
"""Class for building Acks and sending them."""
|
||||
|
||||
def __init__(self, fromcall, tocall, msg_id, transport=MESSAGE_TRANSPORT_APRSIS):
|
||||
super().__init__(fromcall, tocall, msg_id=msg_id, transport=transport)
|
||||
def __init__(self, fromcall, tocall, msg_id):
|
||||
super().__init__(fromcall, tocall, msg_id=msg_id)
|
||||
|
||||
def dict(self):
|
||||
now = datetime.datetime.now()
|
||||
@@ -507,7 +449,7 @@ class AckMessage(Message):
|
||||
if aprsis_client:
|
||||
cl = aprsis_client
|
||||
else:
|
||||
cl = self.get_transport()
|
||||
cl = client.factory.create().client
|
||||
log_message(
|
||||
"Sending ack",
|
||||
str(self).rstrip("\n"),
|
||||
@@ -524,10 +466,8 @@ class SendAckThread(threads.APRSDThread):
|
||||
self.ack = ack
|
||||
super().__init__(f"SendAck-{self.ack.id}")
|
||||
|
||||
@trace.trace
|
||||
def loop(self):
|
||||
"""Separate thread to send acks with retries."""
|
||||
LOG.debug("SendAckThread loop start")
|
||||
send_now = False
|
||||
if self.ack.last_send_attempt == self.ack.retry_count:
|
||||
# we reached the send limit, don't send again
|
||||
@@ -552,7 +492,7 @@ class SendAckThread(threads.APRSDThread):
|
||||
send_now = True
|
||||
|
||||
if send_now:
|
||||
cl = self.ack.get_transport()
|
||||
cl = client.factory.create().client
|
||||
log_message(
|
||||
"Sending ack",
|
||||
str(self.ack).rstrip("\n"),
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
import logging
|
||||
import os
|
||||
import pathlib
|
||||
import pickle
|
||||
|
||||
from aprsd import config as aprsd_config
|
||||
|
||||
|
||||
LOG = logging.getLogger("APRSD")
|
||||
|
||||
|
||||
class ObjectStoreMixin:
|
||||
"""Class 'MIXIN' intended to save/load object data.
|
||||
|
||||
The asumption of how this mixin is used:
|
||||
The using class has to have a:
|
||||
* data in self.data as a dictionary
|
||||
* a self.lock thread lock
|
||||
* Class must specify self.save_file as the location.
|
||||
|
||||
|
||||
When APRSD quits, it calls save()
|
||||
When APRSD Starts, it calls load()
|
||||
aprsd server -f (flush) will wipe all saved objects.
|
||||
"""
|
||||
|
||||
def __len__(self):
|
||||
return len(self.data)
|
||||
|
||||
def get_all(self):
|
||||
with self.lock:
|
||||
return self.data
|
||||
|
||||
def get(self, id):
|
||||
with self.lock:
|
||||
return self.data[id]
|
||||
|
||||
def _init_store(self):
|
||||
sl = self._save_location()
|
||||
if not os.path.exists(sl):
|
||||
LOG.warning(f"Save location {sl} doesn't exist")
|
||||
try:
|
||||
os.makedirs(sl)
|
||||
except Exception as ex:
|
||||
LOG.exception(ex)
|
||||
|
||||
def _save_location(self):
|
||||
save_location = self.config.get("aprsd.save_location", None)
|
||||
if not save_location:
|
||||
save_location = aprsd_config.DEFAULT_CONFIG_DIR
|
||||
return save_location
|
||||
|
||||
def _save_filename(self):
|
||||
save_location = self._save_location()
|
||||
|
||||
return "{}/{}.p".format(
|
||||
save_location,
|
||||
self.__class__.__name__.lower(),
|
||||
)
|
||||
|
||||
def _dump(self):
|
||||
dump = {}
|
||||
with self.lock:
|
||||
for key in self.data.keys():
|
||||
dump[key] = self.data[key]
|
||||
|
||||
return dump
|
||||
|
||||
def save(self):
|
||||
"""Save any queued to disk?"""
|
||||
if len(self) > 0:
|
||||
LOG.info(f"{self.__class__.__name__}::Saving {len(self)} entries to disk at {self._save_location()}")
|
||||
with open(self._save_filename(), "wb+") as fp:
|
||||
pickle.dump(self._dump(), fp)
|
||||
else:
|
||||
LOG.debug(
|
||||
"{} Nothing to save, flushing old save file '{}'".format(
|
||||
self.__class__.__name__,
|
||||
self._save_filename(),
|
||||
),
|
||||
)
|
||||
self.flush()
|
||||
|
||||
def load(self):
|
||||
if os.path.exists(self._save_filename()):
|
||||
try:
|
||||
with open(self._save_filename(), "rb") as fp:
|
||||
raw = pickle.load(fp)
|
||||
if raw:
|
||||
self.data = raw
|
||||
LOG.debug(
|
||||
f"{self.__class__.__name__}::Loaded {len(self)} entries from disk.",
|
||||
)
|
||||
LOG.debug(f"{self.data}")
|
||||
except pickle.UnpicklingError as ex:
|
||||
LOG.error(f"Failed to UnPickle {self._save_filename()}")
|
||||
LOG.error(ex)
|
||||
self.data = {}
|
||||
|
||||
def flush(self):
|
||||
"""Nuke the old pickle file that stored the old results from last aprsd run."""
|
||||
if os.path.exists(self._save_filename()):
|
||||
pathlib.Path(self._save_filename()).unlink()
|
||||
with self.lock:
|
||||
self.data = {}
|
||||
+50
-11
@@ -3,7 +3,7 @@ import logging
|
||||
import threading
|
||||
import time
|
||||
|
||||
from aprsd import utils
|
||||
from aprsd import objectstore, utils
|
||||
|
||||
|
||||
LOG = logging.getLogger("APRSD")
|
||||
@@ -29,6 +29,7 @@ class PacketList:
|
||||
cls._instance = super().__new__(cls)
|
||||
cls._instance.packet_list = utils.RingBuffer(1000)
|
||||
cls._instance.lock = threading.Lock()
|
||||
cls._instance.config = kwargs["config"]
|
||||
return cls._instance
|
||||
|
||||
def __init__(self, config=None):
|
||||
@@ -50,6 +51,7 @@ class PacketList:
|
||||
else:
|
||||
self.total_recv += 1
|
||||
self.packet_list.append(packet)
|
||||
SeenList().update_seen(packet)
|
||||
|
||||
def get(self):
|
||||
with self.lock:
|
||||
@@ -64,18 +66,20 @@ class PacketList:
|
||||
return self.total_tx
|
||||
|
||||
|
||||
class WatchList:
|
||||
class WatchList(objectstore.ObjectStoreMixin):
|
||||
"""Global watch list and info for callsigns."""
|
||||
|
||||
_instance = None
|
||||
callsigns = {}
|
||||
data = {}
|
||||
config = None
|
||||
|
||||
def __new__(cls, *args, **kwargs):
|
||||
if cls._instance is None:
|
||||
cls._instance = super().__new__(cls)
|
||||
cls._instance.lock = threading.Lock()
|
||||
cls.callsigns = {}
|
||||
cls._instance.config = kwargs["config"]
|
||||
cls._instance.data = {}
|
||||
cls._instance._init_store()
|
||||
return cls._instance
|
||||
|
||||
def __init__(self, config=None):
|
||||
@@ -90,7 +94,7 @@ class WatchList:
|
||||
# a beacon from a callsign or some other mechanism to find
|
||||
# last time a message was seen by aprs-is. For now this
|
||||
# is all we can do.
|
||||
self.callsigns[call] = {
|
||||
self.data[call] = {
|
||||
"last": datetime.datetime.now(),
|
||||
"packets": utils.RingBuffer(
|
||||
ring_size,
|
||||
@@ -104,17 +108,18 @@ class WatchList:
|
||||
return False
|
||||
|
||||
def callsign_in_watchlist(self, callsign):
|
||||
return callsign in self.callsigns
|
||||
return callsign in self.data
|
||||
|
||||
def update_seen(self, packet):
|
||||
callsign = packet["from"]
|
||||
if self.callsign_in_watchlist(callsign):
|
||||
self.callsigns[callsign]["last"] = datetime.datetime.now()
|
||||
self.callsigns[callsign]["packets"].append(packet)
|
||||
with self.lock:
|
||||
callsign = packet["from"]
|
||||
if self.callsign_in_watchlist(callsign):
|
||||
self.data[callsign]["last"] = datetime.datetime.now()
|
||||
self.data[callsign]["packets"].append(packet)
|
||||
|
||||
def last_seen(self, callsign):
|
||||
if self.callsign_in_watchlist(callsign):
|
||||
return self.callsigns[callsign]["last"]
|
||||
return self.data[callsign]["last"]
|
||||
|
||||
def age(self, callsign):
|
||||
now = datetime.datetime.now()
|
||||
@@ -149,6 +154,40 @@ class WatchList:
|
||||
return False
|
||||
|
||||
|
||||
class SeenList(objectstore.ObjectStoreMixin):
|
||||
"""Global callsign seen list."""
|
||||
|
||||
_instance = None
|
||||
data = {}
|
||||
config = None
|
||||
|
||||
def __new__(cls, *args, **kwargs):
|
||||
if cls._instance is None:
|
||||
cls._instance = super().__new__(cls)
|
||||
cls._instance.lock = threading.Lock()
|
||||
cls._instance.config = kwargs["config"]
|
||||
cls._instance.data = {}
|
||||
cls._instance._init_store()
|
||||
return cls._instance
|
||||
|
||||
def update_seen(self, packet):
|
||||
callsign = None
|
||||
if "fromcall" in packet:
|
||||
callsign = packet["fromcall"]
|
||||
elif "from" in packet:
|
||||
callsign = packet["from"]
|
||||
else:
|
||||
LOG.warning(f"Can't find FROM in packet {packet}")
|
||||
return
|
||||
if callsign not in self.data:
|
||||
self.data[callsign] = {
|
||||
"last": None,
|
||||
"count": 0,
|
||||
}
|
||||
self.data[callsign]["last"] = str(datetime.datetime.now())
|
||||
self.data[callsign]["count"] += 1
|
||||
|
||||
|
||||
def get_packet_type(packet):
|
||||
"""Decode the packet type from the packet."""
|
||||
|
||||
|
||||
+127
-23
@@ -6,27 +6,25 @@ import inspect
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import textwrap
|
||||
import threading
|
||||
|
||||
import pluggy
|
||||
from thesmuggler import smuggle
|
||||
|
||||
import aprsd
|
||||
from aprsd import client, messaging, packets, threads
|
||||
|
||||
|
||||
# setup the global logger
|
||||
LOG = logging.getLogger("APRSD")
|
||||
|
||||
hookspec = pluggy.HookspecMarker("aprsd")
|
||||
hookimpl = pluggy.HookimplMarker("aprsd")
|
||||
|
||||
CORE_MESSAGE_PLUGINS = [
|
||||
"aprsd.plugins.email.EmailPlugin",
|
||||
"aprsd.plugins.fortune.FortunePlugin",
|
||||
"aprsd.plugins.location.LocationPlugin",
|
||||
"aprsd.plugins.ping.PingPlugin",
|
||||
"aprsd.plugins.query.QueryPlugin",
|
||||
"aprsd.plugins.stock.StockPlugin",
|
||||
"aprsd.plugins.time.TimePlugin",
|
||||
"aprsd.plugins.weather.USWeatherPlugin",
|
||||
"aprsd.plugins.version.VersionPlugin",
|
||||
@@ -36,8 +34,11 @@ CORE_NOTIFY_PLUGINS = [
|
||||
"aprsd.plugins.notify.NotifySeenPlugin",
|
||||
]
|
||||
|
||||
hookspec = pluggy.HookspecMarker("aprsd")
|
||||
hookimpl = pluggy.HookimplMarker("aprsd")
|
||||
|
||||
class APRSDCommandSpec:
|
||||
|
||||
class APRSDPluginSpec:
|
||||
"""A hook specification namespace."""
|
||||
|
||||
@hookspec
|
||||
@@ -51,7 +52,7 @@ class APRSDPluginBase(metaclass=abc.ABCMeta):
|
||||
config = None
|
||||
rx_count = 0
|
||||
tx_count = 0
|
||||
version = "1.0"
|
||||
version = aprsd.__version__
|
||||
|
||||
# Holds the list of APRSDThreads that the plugin creates
|
||||
threads = []
|
||||
@@ -62,11 +63,8 @@ class APRSDPluginBase(metaclass=abc.ABCMeta):
|
||||
self.config = config
|
||||
self.message_counter = 0
|
||||
self.setup()
|
||||
threads = self.create_threads()
|
||||
if threads:
|
||||
self.threads = threads
|
||||
if self.threads:
|
||||
self.start_threads()
|
||||
self.threads = self.create_threads() or []
|
||||
self.start_threads()
|
||||
|
||||
def start_threads(self):
|
||||
if self.enabled and self.threads:
|
||||
@@ -96,10 +94,8 @@ class APRSDPluginBase(metaclass=abc.ABCMeta):
|
||||
def message_count(self):
|
||||
return self.message_counter
|
||||
|
||||
@property
|
||||
def version(self):
|
||||
"""Version"""
|
||||
raise NotImplementedError
|
||||
def help(self):
|
||||
return "Help!"
|
||||
|
||||
@abc.abstractmethod
|
||||
def setup(self):
|
||||
@@ -122,7 +118,6 @@ class APRSDPluginBase(metaclass=abc.ABCMeta):
|
||||
if isinstance(thread, threads.APRSDThread):
|
||||
thread.stop()
|
||||
|
||||
@hookimpl
|
||||
@abc.abstractmethod
|
||||
def filter(self, packet):
|
||||
pass
|
||||
@@ -158,20 +153,28 @@ class APRSDWatchListPluginBase(APRSDPluginBase, metaclass=abc.ABCMeta):
|
||||
)
|
||||
# make sure the timeout is set or this doesn't work
|
||||
if watch_list:
|
||||
aprs_client = client.get_client()
|
||||
aprs_client = client.factory.create().client
|
||||
filter_str = "b/{}".format("/".join(watch_list))
|
||||
aprs_client.set_filter(filter_str)
|
||||
else:
|
||||
LOG.warning("Watch list enabled, but no callsigns set.")
|
||||
|
||||
@hookimpl
|
||||
def filter(self, packet):
|
||||
result = messaging.NULL_MESSAGE
|
||||
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()
|
||||
try:
|
||||
result = self.process(packet)
|
||||
except Exception as ex:
|
||||
LOG.error(
|
||||
"Plugin {} failed to process packet {}".format(
|
||||
self.__class__, ex,
|
||||
),
|
||||
)
|
||||
if result:
|
||||
self.tx_inc()
|
||||
wl.update_seen(packet)
|
||||
@@ -199,6 +202,12 @@ class APRSDRegexCommandPluginBase(APRSDPluginBase, metaclass=abc.ABCMeta):
|
||||
"""The regex to match from the caller"""
|
||||
raise NotImplementedError
|
||||
|
||||
def help(self):
|
||||
return "{}: {}".format(
|
||||
self.command_name.lower(),
|
||||
self.command_regex,
|
||||
)
|
||||
|
||||
def setup(self):
|
||||
"""Do any plugin setup here."""
|
||||
self.enabled = True
|
||||
@@ -221,15 +230,96 @@ class APRSDRegexCommandPluginBase(APRSDPluginBase, metaclass=abc.ABCMeta):
|
||||
if re.search(self.command_regex, message):
|
||||
self.rx_inc()
|
||||
if self.enabled:
|
||||
result = self.process(packet)
|
||||
try:
|
||||
result = self.process(packet)
|
||||
except Exception as ex:
|
||||
LOG.error(
|
||||
"Plugin {} failed to process packet {}".format(
|
||||
self.__class__, ex,
|
||||
),
|
||||
)
|
||||
LOG.exception(ex)
|
||||
if result:
|
||||
self.tx_inc()
|
||||
else:
|
||||
LOG.warning(f"{self.__class__} isn't enabled.")
|
||||
result = f"{self.__class__.__name__} isn't enabled"
|
||||
LOG.warning(result)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
class APRSFIKEYMixin:
|
||||
"""Mixin class to enable checking the existence of the aprs.fi apiKey."""
|
||||
|
||||
def ensure_aprs_fi_key(self):
|
||||
try:
|
||||
self.config.check_option(["services", "aprs.fi", "apiKey"])
|
||||
self.enabled = True
|
||||
except Exception as ex:
|
||||
LOG.error(f"Failed to find config aprs.fi:apikey {ex}")
|
||||
self.enabled = False
|
||||
|
||||
|
||||
class HelpPlugin(APRSDRegexCommandPluginBase):
|
||||
"""Help Plugin that is always enabled.
|
||||
|
||||
This plugin is in this file to prevent a circular import.
|
||||
"""
|
||||
|
||||
version = "1.0"
|
||||
command_regex = "^[hH]"
|
||||
command_name = "help"
|
||||
|
||||
def help(self):
|
||||
return "Help: send APRS help or help <plugin>"
|
||||
|
||||
def process(self, packet):
|
||||
LOG.info("HelpPlugin")
|
||||
# fromcall = packet.get("from")
|
||||
message = packet.get("message_text", None)
|
||||
# ack = packet.get("msgNo", "0")
|
||||
a = re.search(r"^.*\s+(.*)", message)
|
||||
command_name = None
|
||||
if a is not None:
|
||||
command_name = a.group(1).lower()
|
||||
|
||||
pm = PluginManager()
|
||||
|
||||
if command_name and "?" not in command_name:
|
||||
# user wants help for a specific plugin
|
||||
reply = None
|
||||
for p in pm.get_plugins():
|
||||
if (
|
||||
p.enabled and isinstance(p, APRSDRegexCommandPluginBase)
|
||||
and p.command_name.lower() == command_name
|
||||
):
|
||||
reply = p.help()
|
||||
|
||||
if reply:
|
||||
return reply
|
||||
|
||||
list = []
|
||||
for p in pm.get_plugins():
|
||||
LOG.debug(p)
|
||||
if p.enabled and isinstance(p, APRSDRegexCommandPluginBase):
|
||||
name = p.command_name.lower()
|
||||
if name not in list and "help" not in name:
|
||||
list.append(name)
|
||||
|
||||
list.sort()
|
||||
reply = " ".join(list)
|
||||
lines = textwrap.wrap(reply, 60)
|
||||
replies = ["Send APRS MSG of 'help' or 'help <plugin>'"]
|
||||
for line in lines:
|
||||
replies.append(f"plugins: {line}")
|
||||
|
||||
for entry in replies:
|
||||
LOG.debug(f"{len(entry)} {entry}")
|
||||
|
||||
LOG.debug(f"{replies}")
|
||||
return replies
|
||||
|
||||
|
||||
class PluginManager:
|
||||
# The singleton instance object for this class
|
||||
_instance = None
|
||||
@@ -255,6 +345,10 @@ class PluginManager:
|
||||
if config:
|
||||
self.config = config
|
||||
|
||||
def _init(self):
|
||||
self._pluggy_pm = pluggy.PluginManager("aprsd")
|
||||
self._pluggy_pm.add_hookspecs(APRSDPluginSpec)
|
||||
|
||||
def load_plugins_from_path(self, module_path):
|
||||
if not os.path.exists(module_path):
|
||||
LOG.error(f"plugin path '{module_path}' doesn't exist.")
|
||||
@@ -355,9 +449,12 @@ class PluginManager:
|
||||
"""Create the plugin manager and register plugins."""
|
||||
|
||||
LOG.info("Loading APRSD Plugins")
|
||||
self._init()
|
||||
# Help plugin is always enabled.
|
||||
_help = HelpPlugin(self.config)
|
||||
self._pluggy_pm.register(_help)
|
||||
|
||||
enabled_plugins = self.config["aprsd"].get("enabled_plugins", None)
|
||||
self._pluggy_pm = pluggy.PluginManager("aprsd")
|
||||
self._pluggy_pm.add_hookspecs(APRSDCommandSpec)
|
||||
if enabled_plugins:
|
||||
for p_name in enabled_plugins:
|
||||
self._load_plugin(p_name)
|
||||
@@ -383,6 +480,13 @@ class PluginManager:
|
||||
with self.lock:
|
||||
return self._pluggy_pm.hook.filter(packet=packet)
|
||||
|
||||
def stop(self):
|
||||
"""Stop all threads created by all plugins."""
|
||||
with self.lock:
|
||||
for p in self.get_plugins():
|
||||
if hasattr(p, "stop_threads"):
|
||||
p.stop_threads()
|
||||
|
||||
def register_msg(self, obj):
|
||||
"""Register the plugin."""
|
||||
self._pluggy_pm.register(obj)
|
||||
|
||||
+83
-56
@@ -5,6 +5,7 @@ import imaplib
|
||||
import logging
|
||||
import re
|
||||
import smtplib
|
||||
import threading
|
||||
import time
|
||||
|
||||
import imapclient
|
||||
@@ -15,17 +16,52 @@ from aprsd import messaging, plugin, stats, threads, trace
|
||||
|
||||
LOG = logging.getLogger("APRSD")
|
||||
|
||||
# This gets forced set from main.py prior to being used internally
|
||||
CONFIG = {}
|
||||
check_email_delay = 60
|
||||
|
||||
class EmailInfo:
|
||||
"""A singleton thread safe mechanism for the global check_email_delay.
|
||||
|
||||
This has to be done because we have 2 separate threads that access
|
||||
the delay value.
|
||||
1) when EmailPlugin runs from a user message and
|
||||
2) when the background EmailThread runs to check email.
|
||||
|
||||
Access the check email delay with
|
||||
EmailInfo().delay
|
||||
|
||||
Set it with
|
||||
EmailInfo().delay = 100
|
||||
or
|
||||
EmailInfo().delay += 10
|
||||
|
||||
"""
|
||||
|
||||
_instance = None
|
||||
|
||||
def __new__(cls, *args, **kwargs):
|
||||
"""This magic turns this into a singleton."""
|
||||
if cls._instance is None:
|
||||
cls._instance = super().__new__(cls)
|
||||
cls._instance.lock = threading.Lock()
|
||||
cls._instance._delay = 60
|
||||
return cls._instance
|
||||
|
||||
@property
|
||||
def delay(self):
|
||||
with self.lock:
|
||||
return self._delay
|
||||
|
||||
@delay.setter
|
||||
def delay(self, val):
|
||||
with self.lock:
|
||||
self._delay = val
|
||||
|
||||
|
||||
class EmailPlugin(plugin.APRSDRegexCommandPluginBase):
|
||||
"""Email Plugin."""
|
||||
|
||||
version = "1.0"
|
||||
command_regex = "^-.*"
|
||||
command_name = "email"
|
||||
short_description = "Send and Receive email"
|
||||
|
||||
# message_number:time combos so we don't resend the same email in
|
||||
# five mins {int:int}
|
||||
@@ -34,8 +70,6 @@ class EmailPlugin(plugin.APRSDRegexCommandPluginBase):
|
||||
|
||||
def setup(self):
|
||||
"""Ensure that email is enabled and start the thread."""
|
||||
global CONFIG
|
||||
CONFIG = self.config
|
||||
|
||||
email_enabled = self.config["aprsd"]["email"].get("enabled", False)
|
||||
validation = self.config["aprsd"]["email"].get("validate", False)
|
||||
@@ -81,7 +115,7 @@ class EmailPlugin(plugin.APRSDRegexCommandPluginBase):
|
||||
r = re.search("^-([0-9])[0-9]*$", message)
|
||||
if r is not None:
|
||||
LOG.debug("RESEND EMAIL")
|
||||
resend_email(r.group(1), fromcall)
|
||||
resend_email(self.config, r.group(1), fromcall)
|
||||
reply = messaging.NULL_MESSAGE
|
||||
# -user@address.com body of email
|
||||
elif re.search(r"^-([A-Za-z0-9_\-\.@]+) (.*)", message):
|
||||
@@ -91,7 +125,7 @@ class EmailPlugin(plugin.APRSDRegexCommandPluginBase):
|
||||
to_addr = a.group(1)
|
||||
content = a.group(2)
|
||||
|
||||
email_address = get_email_from_shortcut(to_addr)
|
||||
email_address = get_email_from_shortcut(self.config, to_addr)
|
||||
if not email_address:
|
||||
reply = "Bad email address"
|
||||
return reply
|
||||
@@ -114,7 +148,7 @@ class EmailPlugin(plugin.APRSDRegexCommandPluginBase):
|
||||
too_soon = 1
|
||||
if not too_soon or ack == 0:
|
||||
LOG.info(f"Send email '{content}'")
|
||||
send_result = email.send_email(to_addr, content)
|
||||
send_result = send_email(self.config, to_addr, content)
|
||||
reply = messaging.NULL_MESSAGE
|
||||
if send_result != 0:
|
||||
reply = f"-{to_addr} failed"
|
||||
@@ -143,10 +177,9 @@ class EmailPlugin(plugin.APRSDRegexCommandPluginBase):
|
||||
return reply
|
||||
|
||||
|
||||
def _imap_connect():
|
||||
global CONFIG
|
||||
imap_port = CONFIG["aprsd"]["email"]["imap"].get("port", 143)
|
||||
use_ssl = CONFIG["aprsd"]["email"]["imap"].get("use_ssl", False)
|
||||
def _imap_connect(config):
|
||||
imap_port = config["aprsd"]["email"]["imap"].get("port", 143)
|
||||
use_ssl = config["aprsd"]["email"]["imap"].get("use_ssl", False)
|
||||
# host = CONFIG["aprsd"]["email"]["imap"]["host"]
|
||||
# msg = "{}{}:{}".format("TLS " if use_ssl else "", host, imap_port)
|
||||
# LOG.debug("Connect to IMAP host {} with user '{}'".
|
||||
@@ -154,7 +187,7 @@ def _imap_connect():
|
||||
|
||||
try:
|
||||
server = imapclient.IMAPClient(
|
||||
CONFIG["aprsd"]["email"]["imap"]["host"],
|
||||
config["aprsd"]["email"]["imap"]["host"],
|
||||
port=imap_port,
|
||||
use_uid=True,
|
||||
ssl=use_ssl,
|
||||
@@ -166,8 +199,8 @@ def _imap_connect():
|
||||
|
||||
try:
|
||||
server.login(
|
||||
CONFIG["aprsd"]["email"]["imap"]["login"],
|
||||
CONFIG["aprsd"]["email"]["imap"]["password"],
|
||||
config["aprsd"]["email"]["imap"]["login"],
|
||||
config["aprsd"]["email"]["imap"]["password"],
|
||||
)
|
||||
except (imaplib.IMAP4.error, Exception) as e:
|
||||
msg = getattr(e, "message", repr(e))
|
||||
@@ -183,15 +216,15 @@ def _imap_connect():
|
||||
return server
|
||||
|
||||
|
||||
def _smtp_connect():
|
||||
host = CONFIG["aprsd"]["email"]["smtp"]["host"]
|
||||
smtp_port = CONFIG["aprsd"]["email"]["smtp"]["port"]
|
||||
use_ssl = CONFIG["aprsd"]["email"]["smtp"].get("use_ssl", False)
|
||||
def _smtp_connect(config):
|
||||
host = config["aprsd"]["email"]["smtp"]["host"]
|
||||
smtp_port = config["aprsd"]["email"]["smtp"]["port"]
|
||||
use_ssl = config["aprsd"]["email"]["smtp"].get("use_ssl", False)
|
||||
msg = "{}{}:{}".format("SSL " if use_ssl else "", host, smtp_port)
|
||||
LOG.debug(
|
||||
"Connect to SMTP host {} with user '{}'".format(
|
||||
msg,
|
||||
CONFIG["aprsd"]["email"]["imap"]["login"],
|
||||
config["aprsd"]["email"]["imap"]["login"],
|
||||
),
|
||||
)
|
||||
|
||||
@@ -214,15 +247,15 @@ def _smtp_connect():
|
||||
|
||||
LOG.debug(f"Connected to smtp host {msg}")
|
||||
|
||||
debug = CONFIG["aprsd"]["email"]["smtp"].get("debug", False)
|
||||
debug = config["aprsd"]["email"]["smtp"].get("debug", False)
|
||||
if debug:
|
||||
server.set_debuglevel(5)
|
||||
server.sendmail = trace.trace(server.sendmail)
|
||||
|
||||
try:
|
||||
server.login(
|
||||
CONFIG["aprsd"]["email"]["smtp"]["login"],
|
||||
CONFIG["aprsd"]["email"]["smtp"]["password"],
|
||||
config["aprsd"]["email"]["smtp"]["login"],
|
||||
config["aprsd"]["email"]["smtp"]["password"],
|
||||
)
|
||||
except Exception:
|
||||
LOG.error("Couldn't connect to SMTP Server")
|
||||
@@ -273,9 +306,9 @@ def validate_shortcuts(config):
|
||||
)
|
||||
|
||||
|
||||
def get_email_from_shortcut(addr):
|
||||
if CONFIG["aprsd"]["email"].get("shortcuts", False):
|
||||
return CONFIG["aprsd"]["email"]["shortcuts"].get(addr, addr)
|
||||
def get_email_from_shortcut(config, addr):
|
||||
if config["aprsd"]["email"].get("shortcuts", False):
|
||||
return config["aprsd"]["email"]["shortcuts"].get(addr, addr)
|
||||
else:
|
||||
return addr
|
||||
|
||||
@@ -286,9 +319,9 @@ def validate_email_config(config, disable_validation=False):
|
||||
This helps with failing early during startup.
|
||||
"""
|
||||
LOG.info("Checking IMAP configuration")
|
||||
imap_server = _imap_connect()
|
||||
imap_server = _imap_connect(config)
|
||||
LOG.info("Checking SMTP configuration")
|
||||
smtp_server = _smtp_connect()
|
||||
smtp_server = _smtp_connect(config)
|
||||
|
||||
# Now validate and flag any shortcuts as invalid
|
||||
if not disable_validation:
|
||||
@@ -398,34 +431,32 @@ def parse_email(msgid, data, server):
|
||||
|
||||
|
||||
@trace.trace
|
||||
def send_email(to_addr, content):
|
||||
global check_email_delay
|
||||
|
||||
shortcuts = CONFIG["aprsd"]["email"]["shortcuts"]
|
||||
email_address = get_email_from_shortcut(to_addr)
|
||||
def send_email(config, to_addr, content):
|
||||
shortcuts = config["aprsd"]["email"]["shortcuts"]
|
||||
email_address = get_email_from_shortcut(config, to_addr)
|
||||
LOG.info("Sending Email_________________")
|
||||
|
||||
if to_addr in shortcuts:
|
||||
LOG.info("To : " + to_addr)
|
||||
to_addr = email_address
|
||||
LOG.info(" (" + to_addr + ")")
|
||||
subject = CONFIG["ham"]["callsign"]
|
||||
subject = config["ham"]["callsign"]
|
||||
# content = content + "\n\n(NOTE: reply with one line)"
|
||||
LOG.info("Subject : " + subject)
|
||||
LOG.info("Body : " + content)
|
||||
|
||||
# check email more often since there's activity right now
|
||||
check_email_delay = 60
|
||||
EmailInfo().delay = 60
|
||||
|
||||
msg = MIMEText(content)
|
||||
msg["Subject"] = subject
|
||||
msg["From"] = CONFIG["aprsd"]["email"]["smtp"]["login"]
|
||||
msg["From"] = config["aprsd"]["email"]["smtp"]["login"]
|
||||
msg["To"] = to_addr
|
||||
server = _smtp_connect()
|
||||
server = _smtp_connect(config)
|
||||
if server:
|
||||
try:
|
||||
server.sendmail(
|
||||
CONFIG["aprsd"]["email"]["smtp"]["login"],
|
||||
config["aprsd"]["email"]["smtp"]["login"],
|
||||
[to_addr],
|
||||
msg.as_string(),
|
||||
)
|
||||
@@ -440,20 +471,19 @@ def send_email(to_addr, content):
|
||||
|
||||
|
||||
@trace.trace
|
||||
def resend_email(count, fromcall):
|
||||
global check_email_delay
|
||||
def resend_email(config, count, fromcall):
|
||||
date = datetime.datetime.now()
|
||||
month = date.strftime("%B")[:3] # Nov, Mar, Apr
|
||||
day = date.day
|
||||
year = date.year
|
||||
today = f"{day}-{month}-{year}"
|
||||
|
||||
shortcuts = CONFIG["aprsd"]["email"]["shortcuts"]
|
||||
shortcuts = config["aprsd"]["email"]["shortcuts"]
|
||||
# swap key/value
|
||||
shortcuts_inverted = {v: k for k, v in shortcuts.items()}
|
||||
|
||||
try:
|
||||
server = _imap_connect()
|
||||
server = _imap_connect(config)
|
||||
except Exception as e:
|
||||
LOG.exception("Failed to Connect to IMAP. Cannot resend email ", e)
|
||||
return
|
||||
@@ -493,7 +523,7 @@ def resend_email(count, fromcall):
|
||||
reply = "-" + from_addr + " * " + body.decode(errors="ignore")
|
||||
# messaging.send_message(fromcall, reply)
|
||||
msg = messaging.TextMessage(
|
||||
CONFIG["aprs"]["login"],
|
||||
config["aprs"]["login"],
|
||||
fromcall,
|
||||
reply,
|
||||
)
|
||||
@@ -515,11 +545,11 @@ def resend_email(count, fromcall):
|
||||
str(s).zfill(2),
|
||||
)
|
||||
# messaging.send_message(fromcall, reply)
|
||||
msg = messaging.TextMessage(CONFIG["aprs"]["login"], fromcall, reply)
|
||||
msg = messaging.TextMessage(config["aprs"]["login"], fromcall, reply)
|
||||
msg.send()
|
||||
|
||||
# check email more often since we're resending one now
|
||||
check_email_delay = 60
|
||||
EmailInfo().delay = 60
|
||||
|
||||
server.logout()
|
||||
# end resend_email()
|
||||
@@ -533,27 +563,24 @@ class APRSDEmailThread(threads.APRSDThread):
|
||||
self.past = datetime.datetime.now()
|
||||
|
||||
def loop(self):
|
||||
global check_email_delay
|
||||
|
||||
check_email_delay = 60
|
||||
time.sleep(5)
|
||||
stats.APRSDStats().email_thread_update()
|
||||
# always sleep for 5 seconds and see if we need to check email
|
||||
# This allows CTRL-C to stop the execution of this loop sooner
|
||||
# than check_email_delay time
|
||||
now = datetime.datetime.now()
|
||||
if now - self.past > datetime.timedelta(seconds=check_email_delay):
|
||||
if now - self.past > datetime.timedelta(seconds=EmailInfo().delay):
|
||||
# It's time to check email
|
||||
|
||||
# slowly increase delay every iteration, max out at 300 seconds
|
||||
# any send/receive/resend activity will reset this to 60 seconds
|
||||
if check_email_delay < 300:
|
||||
check_email_delay += 1
|
||||
if EmailInfo().delay < 300:
|
||||
EmailInfo().delay += 10
|
||||
LOG.debug(
|
||||
"check_email_delay is " + str(check_email_delay) + " seconds",
|
||||
f"check_email_delay is {EmailInfo().delay} seconds ",
|
||||
)
|
||||
|
||||
shortcuts = CONFIG["aprsd"]["email"]["shortcuts"]
|
||||
shortcuts = self.config["aprsd"]["email"]["shortcuts"]
|
||||
# swap key/value
|
||||
shortcuts_inverted = {v: k for k, v in shortcuts.items()}
|
||||
|
||||
@@ -564,7 +591,7 @@ class APRSDEmailThread(threads.APRSDThread):
|
||||
today = f"{day}-{month}-{year}"
|
||||
|
||||
try:
|
||||
server = _imap_connect()
|
||||
server = _imap_connect(self.config)
|
||||
except Exception as e:
|
||||
LOG.exception("IMAP failed to connect.", e)
|
||||
return True
|
||||
@@ -658,7 +685,7 @@ class APRSDEmailThread(threads.APRSDThread):
|
||||
LOG.exception("Couldn't remove seen flag from email", e)
|
||||
|
||||
# check email more often since we just received an email
|
||||
check_email_delay = 60
|
||||
EmailInfo().delay = 60
|
||||
|
||||
# reset clock
|
||||
LOG.debug("Done looping over Server.fetch, logging out.")
|
||||
|
||||
@@ -11,9 +11,18 @@ LOG = logging.getLogger("APRSD")
|
||||
class FortunePlugin(plugin.APRSDRegexCommandPluginBase):
|
||||
"""Fortune."""
|
||||
|
||||
version = "1.0"
|
||||
command_regex = "^[fF]"
|
||||
command_name = "fortune"
|
||||
short_description = "Give me a fortune"
|
||||
|
||||
fortune_path = None
|
||||
|
||||
def setup(self):
|
||||
self.fortune_path = shutil.which("fortune")
|
||||
if not self.fortune_path:
|
||||
self.enabled = False
|
||||
else:
|
||||
self.enabled = True
|
||||
|
||||
@trace.trace
|
||||
def process(self, packet):
|
||||
@@ -25,13 +34,8 @@ class FortunePlugin(plugin.APRSDRegexCommandPluginBase):
|
||||
|
||||
reply = None
|
||||
|
||||
fortune_path = shutil.which("fortune")
|
||||
if not fortune_path:
|
||||
reply = "Fortune command not installed"
|
||||
return reply
|
||||
|
||||
try:
|
||||
cmnd = [fortune_path, "-s", "-n 60"]
|
||||
cmnd = [self.fortune_path, "-s", "-n 60"]
|
||||
command = " ".join(cmnd)
|
||||
output = subprocess.check_output(
|
||||
command,
|
||||
|
||||
@@ -2,18 +2,21 @@ import logging
|
||||
import re
|
||||
import time
|
||||
|
||||
from aprsd import plugin, plugin_utils, trace, utils
|
||||
from aprsd import plugin, plugin_utils, trace
|
||||
|
||||
|
||||
LOG = logging.getLogger("APRSD")
|
||||
|
||||
|
||||
class LocationPlugin(plugin.APRSDRegexCommandPluginBase):
|
||||
class LocationPlugin(plugin.APRSDRegexCommandPluginBase, plugin.APRSFIKEYMixin):
|
||||
"""Location!"""
|
||||
|
||||
version = "1.0"
|
||||
command_regex = "^[lL]"
|
||||
command_name = "location"
|
||||
short_description = "Where in the world is a CALLSIGN's last GPS beacon?"
|
||||
|
||||
def setup(self):
|
||||
self.ensure_aprs_fi_key()
|
||||
|
||||
@trace.trace
|
||||
def process(self, packet):
|
||||
@@ -22,13 +25,6 @@ class LocationPlugin(plugin.APRSDRegexCommandPluginBase):
|
||||
message = packet.get("message_text", None)
|
||||
# ack = packet.get("msgNo", "0")
|
||||
|
||||
# get last location of a callsign, get descriptive name from weather service
|
||||
try:
|
||||
utils.check_config_option(self.config, ["services", "aprs.fi", "apiKey"])
|
||||
except Exception as ex:
|
||||
LOG.error(f"Failed to find config aprs.fi:apikey {ex}")
|
||||
return "No aprs.fi apikey found"
|
||||
|
||||
api_key = self.config["services"]["aprs.fi"]["apiKey"]
|
||||
|
||||
# optional second argument is a callsign to search
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import logging
|
||||
|
||||
from aprsd import messaging, packets, plugin
|
||||
from aprsd import packets, plugin
|
||||
|
||||
|
||||
LOG = logging.getLogger("APRSD")
|
||||
@@ -15,14 +15,10 @@ class NotifySeenPlugin(plugin.APRSDWatchListPluginBase):
|
||||
seen was older than the configured age limit.
|
||||
"""
|
||||
|
||||
version = "1.0"
|
||||
|
||||
def __init__(self, config):
|
||||
"""The aprsd config object is stored."""
|
||||
super().__init__(config)
|
||||
short_description = "Notify me when a CALLSIGN is recently seen on APRS-IS"
|
||||
|
||||
def process(self, packet):
|
||||
LOG.info("BaseNotifyPlugin")
|
||||
LOG.info("NotifySeenPlugin")
|
||||
|
||||
notify_callsign = self.config["aprsd"]["watch_list"]["alert_callsign"]
|
||||
fromcall = packet.get("from")
|
||||
@@ -50,4 +46,3 @@ class NotifySeenPlugin(plugin.APRSDWatchListPluginBase):
|
||||
wl.max_delta(),
|
||||
),
|
||||
)
|
||||
return messaging.NULL_MESSAGE
|
||||
|
||||
@@ -10,9 +10,9 @@ LOG = logging.getLogger("APRSD")
|
||||
class PingPlugin(plugin.APRSDRegexCommandPluginBase):
|
||||
"""Ping."""
|
||||
|
||||
version = "1.0"
|
||||
command_regex = "^[pP]"
|
||||
command_name = "ping"
|
||||
short_description = "reply with a Pong!"
|
||||
|
||||
@trace.trace
|
||||
def process(self, packet):
|
||||
|
||||
@@ -11,9 +11,9 @@ LOG = logging.getLogger("APRSD")
|
||||
class QueryPlugin(plugin.APRSDRegexCommandPluginBase):
|
||||
"""Query command."""
|
||||
|
||||
version = "1.0"
|
||||
command_regex = r"^\!.*"
|
||||
command_name = "query"
|
||||
short_description = "APRSD Owner command to query messages in the MsgTrack"
|
||||
|
||||
@trace.trace
|
||||
def process(self, packet):
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
import logging
|
||||
import re
|
||||
|
||||
import yfinance as yf
|
||||
|
||||
from aprsd import plugin, trace
|
||||
|
||||
|
||||
LOG = logging.getLogger("APRSD")
|
||||
|
||||
|
||||
class StockPlugin(plugin.APRSDRegexCommandPluginBase):
|
||||
"""Stock market plugin for fetching stock quotes"""
|
||||
|
||||
version = "1.0"
|
||||
command_regex = "^[sS]"
|
||||
command_name = "stock"
|
||||
|
||||
@trace.trace
|
||||
def process(self, packet):
|
||||
LOG.info("StockPlugin")
|
||||
|
||||
# fromcall = packet.get("from")
|
||||
message = packet.get("message_text", None)
|
||||
# ack = packet.get("msgNo", "0")
|
||||
|
||||
a = re.search(r"^.*\s+(.*)", message)
|
||||
if a is not None:
|
||||
searchcall = a.group(1)
|
||||
stock_symbol = searchcall.upper()
|
||||
else:
|
||||
reply = "No stock symbol"
|
||||
return reply
|
||||
|
||||
LOG.info(f"Fetch stock quote for '{stock_symbol}'")
|
||||
|
||||
try:
|
||||
stock = yf.Ticker(stock_symbol)
|
||||
reply = "{} - ask: {} high: {} low: {}".format(
|
||||
stock_symbol,
|
||||
stock.info["ask"],
|
||||
stock.info["dayHigh"],
|
||||
stock.info["dayLow"],
|
||||
)
|
||||
except Exception as e:
|
||||
LOG.error(
|
||||
f"Failed to fetch stock '{stock_symbol}' from yahoo '{e}'",
|
||||
)
|
||||
reply = f"Failed to fetch stock '{stock_symbol}'"
|
||||
|
||||
return reply.rstrip()
|
||||
+16
-25
@@ -5,7 +5,7 @@ import time
|
||||
from opencage.geocoder import OpenCageGeocode
|
||||
import pytz
|
||||
|
||||
from aprsd import fuzzyclock, plugin, plugin_utils, trace, utils
|
||||
from aprsd import fuzzyclock, plugin, plugin_utils, trace
|
||||
|
||||
|
||||
LOG = logging.getLogger("APRSD")
|
||||
@@ -14,9 +14,9 @@ LOG = logging.getLogger("APRSD")
|
||||
class TimePlugin(plugin.APRSDRegexCommandPluginBase):
|
||||
"""Time command."""
|
||||
|
||||
version = "1.0"
|
||||
command_regex = "^[tT]"
|
||||
command_name = "time"
|
||||
short_description = "What is the current local time."
|
||||
|
||||
def _get_local_tz(self):
|
||||
return pytz.timezone(time.strftime("%Z"))
|
||||
@@ -49,12 +49,15 @@ class TimePlugin(plugin.APRSDRegexCommandPluginBase):
|
||||
return self.build_date_str(localzone)
|
||||
|
||||
|
||||
class TimeOpenCageDataPlugin(TimePlugin):
|
||||
class TimeOpenCageDataPlugin(TimePlugin, plugin.APRSFIKEYMixin):
|
||||
"""geocage based timezone fetching."""
|
||||
|
||||
version = "1.0"
|
||||
command_regex = "^[tT]"
|
||||
command_name = "Time"
|
||||
command_name = "time"
|
||||
short_description = "Current time of GPS beacon timezone. Uses OpenCage"
|
||||
|
||||
def setup(self):
|
||||
self.ensure_aprs_fi_key()
|
||||
|
||||
@trace.trace
|
||||
def process(self, packet):
|
||||
@@ -62,13 +65,6 @@ class TimeOpenCageDataPlugin(TimePlugin):
|
||||
message = packet.get("message_text", None)
|
||||
# ack = packet.get("msgNo", "0")
|
||||
|
||||
# get last location of a callsign, get descriptive name from weather service
|
||||
try:
|
||||
utils.check_config_option(self.config, ["services", "aprs.fi", "apiKey"])
|
||||
except Exception as ex:
|
||||
LOG.error(f"Failed to find config aprs.fi:apikey {ex}")
|
||||
return "No aprs.fi apikey found"
|
||||
|
||||
api_key = self.config["services"]["aprs.fi"]["apiKey"]
|
||||
|
||||
# optional second argument is a callsign to search
|
||||
@@ -95,7 +91,7 @@ class TimeOpenCageDataPlugin(TimePlugin):
|
||||
lon = aprs_data["entries"][0]["lng"]
|
||||
|
||||
try:
|
||||
utils.check_config_option(self.config, "opencagedata", "apiKey")
|
||||
self.config.exists("opencagedata.apiKey")
|
||||
except Exception as ex:
|
||||
LOG.error(f"Failed to find config opencage:apiKey {ex}")
|
||||
return "No opencage apiKey found"
|
||||
@@ -115,12 +111,15 @@ class TimeOpenCageDataPlugin(TimePlugin):
|
||||
return self.build_date_str(localzone)
|
||||
|
||||
|
||||
class TimeOWMPlugin(TimePlugin):
|
||||
class TimeOWMPlugin(TimePlugin, plugin.APRSFIKEYMixin):
|
||||
"""OpenWeatherMap based timezone fetching."""
|
||||
|
||||
version = "1.0"
|
||||
command_regex = "^[tT]"
|
||||
command_name = "Time"
|
||||
command_name = "time"
|
||||
short_description = "Current time of GPS beacon's timezone. Uses OpenWeatherMap"
|
||||
|
||||
def setup(self):
|
||||
self.ensure_aprs_fi_key()
|
||||
|
||||
@trace.trace
|
||||
def process(self, packet):
|
||||
@@ -128,13 +127,6 @@ class TimeOWMPlugin(TimePlugin):
|
||||
message = packet.get("message_text", None)
|
||||
# ack = packet.get("msgNo", "0")
|
||||
|
||||
# get last location of a callsign, get descriptive name from weather service
|
||||
try:
|
||||
utils.check_config_option(self.config, ["services", "aprs.fi", "apiKey"])
|
||||
except Exception as ex:
|
||||
LOG.error(f"Failed to find config aprs.fi:apikey {ex}")
|
||||
return "No aprs.fi apikey found"
|
||||
|
||||
# optional second argument is a callsign to search
|
||||
a = re.search(r"^.*\s+(.*)", message)
|
||||
if a is not None:
|
||||
@@ -160,8 +152,7 @@ class TimeOWMPlugin(TimePlugin):
|
||||
lon = aprs_data["entries"][0]["lng"]
|
||||
|
||||
try:
|
||||
utils.check_config_option(
|
||||
self.config,
|
||||
self.config.exists(
|
||||
["services", "openweathermap", "apiKey"],
|
||||
)
|
||||
except Exception as ex:
|
||||
|
||||
@@ -10,9 +10,9 @@ LOG = logging.getLogger("APRSD")
|
||||
class VersionPlugin(plugin.APRSDRegexCommandPluginBase):
|
||||
"""Version of APRSD Plugin."""
|
||||
|
||||
version = "1.0"
|
||||
command_regex = "^[vV]"
|
||||
command_name = "version"
|
||||
short_description = "What is the APRSD Version"
|
||||
|
||||
# message_number:time combos so we don't resend the same email in
|
||||
# five mins {int:int}
|
||||
|
||||
+35
-23
@@ -4,7 +4,7 @@ import re
|
||||
|
||||
import requests
|
||||
|
||||
from aprsd import plugin, plugin_utils, trace, utils
|
||||
from aprsd import plugin, plugin_utils, trace
|
||||
|
||||
|
||||
LOG = logging.getLogger("APRSD")
|
||||
@@ -23,9 +23,9 @@ class USWeatherPlugin(plugin.APRSDRegexCommandPluginBase):
|
||||
"weather" - returns weather near the calling callsign
|
||||
"""
|
||||
|
||||
version = "1.0"
|
||||
command_regex = "^[wW]"
|
||||
command_name = "weather"
|
||||
command_name = "USWeather"
|
||||
short_description = "Provide USA only weather of GPS Beacon location"
|
||||
|
||||
@trace.trace
|
||||
def process(self, packet):
|
||||
@@ -34,7 +34,7 @@ class USWeatherPlugin(plugin.APRSDRegexCommandPluginBase):
|
||||
# message = packet.get("message_text", None)
|
||||
# ack = packet.get("msgNo", "0")
|
||||
try:
|
||||
utils.check_config_option(self.config, ["services", "aprs.fi", "apiKey"])
|
||||
self.config.exists(["services", "aprs.fi", "apiKey"])
|
||||
except Exception as ex:
|
||||
LOG.error(f"Failed to find config aprs.fi:apikey {ex}")
|
||||
return "No aprs.fi apikey found"
|
||||
@@ -86,9 +86,9 @@ class USMetarPlugin(plugin.APRSDRegexCommandPluginBase):
|
||||
|
||||
"""
|
||||
|
||||
version = "1.0"
|
||||
command_regex = "^[metar]"
|
||||
command_name = "Metar"
|
||||
command_name = "USMetar"
|
||||
short_description = "USA only METAR of GPS Beacon location"
|
||||
|
||||
@trace.trace
|
||||
def process(self, packet):
|
||||
@@ -115,10 +115,7 @@ class USMetarPlugin(plugin.APRSDRegexCommandPluginBase):
|
||||
fromcall = fromcall
|
||||
|
||||
try:
|
||||
utils.check_config_option(
|
||||
self.config,
|
||||
["services", "aprs.fi", "apiKey"],
|
||||
)
|
||||
self.config.exists(["services", "aprs.fi", "apiKey"])
|
||||
except Exception as ex:
|
||||
LOG.error(f"Failed to find config aprs.fi:apikey {ex}")
|
||||
return "No aprs.fi apikey found"
|
||||
@@ -181,9 +178,18 @@ class OWMWeatherPlugin(plugin.APRSDRegexCommandPluginBase):
|
||||
|
||||
"""
|
||||
|
||||
version = "1.0"
|
||||
command_regex = "^[wW]"
|
||||
command_name = "Weather"
|
||||
command_name = "OpenWeatherMap"
|
||||
short_description = "OpenWeatherMap weather of GPS Beacon location"
|
||||
|
||||
def help(self):
|
||||
_help = [
|
||||
"openweathermap: Send {} to get weather "
|
||||
"from your location".format(self.command_regex),
|
||||
"openweathermap: Send {} <callsign> to get "
|
||||
"weather from <callsign>".format(self.command_regex),
|
||||
]
|
||||
return _help
|
||||
|
||||
@trace.trace
|
||||
def process(self, packet):
|
||||
@@ -199,7 +205,7 @@ class OWMWeatherPlugin(plugin.APRSDRegexCommandPluginBase):
|
||||
searchcall = fromcall
|
||||
|
||||
try:
|
||||
utils.check_config_option(self.config, ["services", "aprs.fi", "apiKey"])
|
||||
self.config.exists(["services", "aprs.fi", "apiKey"])
|
||||
except Exception as ex:
|
||||
LOG.error(f"Failed to find config aprs.fi:apikey {ex}")
|
||||
return "No aprs.fi apikey found"
|
||||
@@ -220,16 +226,13 @@ class OWMWeatherPlugin(plugin.APRSDRegexCommandPluginBase):
|
||||
lon = aprs_data["entries"][0]["lng"]
|
||||
|
||||
try:
|
||||
utils.check_config_option(
|
||||
self.config,
|
||||
["services", "openweathermap", "apiKey"],
|
||||
)
|
||||
self.config.exists(["services", "openweathermap", "apiKey"])
|
||||
except Exception as ex:
|
||||
LOG.error(f"Failed to find config openweathermap:apiKey {ex}")
|
||||
return "No openweathermap apiKey found"
|
||||
|
||||
try:
|
||||
utils.check_config_option(self.config, ["aprsd", "units"])
|
||||
self.config.exists(["aprsd", "units"])
|
||||
except Exception:
|
||||
LOG.debug("Couldn't find untis in aprsd:services:units")
|
||||
units = "metric"
|
||||
@@ -305,9 +308,18 @@ class AVWXWeatherPlugin(plugin.APRSDRegexCommandPluginBase):
|
||||
docker build -f Dockerfile -t avwx-api:master .
|
||||
"""
|
||||
|
||||
version = "1.0"
|
||||
command_regex = "^[mM]"
|
||||
command_name = "Weather"
|
||||
command_name = "AVWXWeather"
|
||||
short_description = "AVWX weather of GPS Beacon location"
|
||||
|
||||
def help(self):
|
||||
_help = [
|
||||
"avwxweather: Send {} to get weather "
|
||||
"from your location".format(self.command_regex),
|
||||
"avwxweather: Send {} <callsign> to get "
|
||||
"weather from <callsign>".format(self.command_regex),
|
||||
]
|
||||
return _help
|
||||
|
||||
@trace.trace
|
||||
def process(self, packet):
|
||||
@@ -323,7 +335,7 @@ class AVWXWeatherPlugin(plugin.APRSDRegexCommandPluginBase):
|
||||
searchcall = fromcall
|
||||
|
||||
try:
|
||||
utils.check_config_option(self.config, ["services", "aprs.fi", "apiKey"])
|
||||
self.config.exists(["services", "aprs.fi", "apiKey"])
|
||||
except Exception as ex:
|
||||
LOG.error(f"Failed to find config aprs.fi:apikey {ex}")
|
||||
return "No aprs.fi apikey found"
|
||||
@@ -344,13 +356,13 @@ class AVWXWeatherPlugin(plugin.APRSDRegexCommandPluginBase):
|
||||
lon = aprs_data["entries"][0]["lng"]
|
||||
|
||||
try:
|
||||
utils.check_config_option(self.config, ["services", "avwx", "apiKey"])
|
||||
self.config.exists(["services", "avwx", "apiKey"])
|
||||
except Exception as ex:
|
||||
LOG.error(f"Failed to find config avwx:apiKey {ex}")
|
||||
return "No avwx apiKey found"
|
||||
|
||||
try:
|
||||
utils.check_config_option(self.config, ["services", "avwx", "base_url"])
|
||||
self.config.exists(self.config, ["services", "avwx", "base_url"])
|
||||
except Exception as ex:
|
||||
LOG.debug(f"Didn't find avwx:base_url {ex}")
|
||||
base_url = "https://avwx.rest"
|
||||
|
||||
+4
-1
@@ -197,9 +197,11 @@ class APRSDStats:
|
||||
"enabled": p.enabled,
|
||||
"rx": p.rx_count,
|
||||
"tx": p.tx_count,
|
||||
"version": p.version,
|
||||
}
|
||||
|
||||
wl = packets.WatchList()
|
||||
sl = packets.SeenList()
|
||||
|
||||
stats = {
|
||||
"aprsd": {
|
||||
@@ -209,7 +211,8 @@ class APRSDStats:
|
||||
"memory_current_str": utils.human_size(self.memory),
|
||||
"memory_peak": self.memory_peak,
|
||||
"memory_peak_str": utils.human_size(self.memory_peak),
|
||||
"watch_list": wl.callsigns,
|
||||
"watch_list": wl.get_all(),
|
||||
"seen_list": sl.get_all(),
|
||||
},
|
||||
"aprs-is": {
|
||||
"server": self.aprsis_server,
|
||||
|
||||
+14
-99
@@ -8,7 +8,7 @@ import tracemalloc
|
||||
|
||||
import aprslib
|
||||
|
||||
from aprsd import client, kissclient, messaging, packets, plugin, stats, utils
|
||||
from aprsd import client, messaging, packets, plugin, stats, utils
|
||||
|
||||
|
||||
LOG = logging.getLogger("APRSD")
|
||||
@@ -17,7 +17,6 @@ RX_THREAD = "RX"
|
||||
EMAIL_THREAD = "Email"
|
||||
|
||||
rx_msg_queue = queue.Queue(maxsize=20)
|
||||
logging_queue = queue.Queue()
|
||||
msg_queues = {
|
||||
"rx": rx_msg_queue,
|
||||
}
|
||||
@@ -137,9 +136,9 @@ class KeepAliveThread(APRSDThread):
|
||||
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):
|
||||
if not client.KISSClient.is_enabled(self.config):
|
||||
LOG.warning("Resetting connection to APRS-IS.")
|
||||
client.Client().reset()
|
||||
client.factory.create().reset()
|
||||
|
||||
# Check version every hour
|
||||
delta = now - self.checker_time
|
||||
@@ -158,13 +157,13 @@ class APRSDRXThread(APRSDThread):
|
||||
super().__init__("RX_MSG")
|
||||
self.msg_queues = msg_queues
|
||||
self.config = config
|
||||
self._client = client.factory.create()
|
||||
|
||||
def stop(self):
|
||||
self.thread_stop = True
|
||||
client.get_client().stop()
|
||||
client.factory.create().client.stop()
|
||||
|
||||
def loop(self):
|
||||
aprs_client = client.get_client()
|
||||
|
||||
# setup the consumer of messages and block until a messages
|
||||
try:
|
||||
@@ -177,7 +176,9 @@ class APRSDRXThread(APRSDThread):
|
||||
# and the aprslib developer didn't want to allow a PR to add
|
||||
# kwargs. :(
|
||||
# https://github.com/rossengeorgiev/aprs-python/pull/56
|
||||
aprs_client.consumer(self.process_packet, raw=False, blocking=False)
|
||||
self._client.client.consumer(
|
||||
self.process_packet, raw=False, blocking=False,
|
||||
)
|
||||
|
||||
except aprslib.exceptions.ConnectionDrop:
|
||||
LOG.error("Connection dropped, reconnecting")
|
||||
@@ -185,21 +186,21 @@ class APRSDRXThread(APRSDThread):
|
||||
# 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()
|
||||
self._client.reset()
|
||||
# Continue to loop
|
||||
return True
|
||||
|
||||
def process_packet(self, packet):
|
||||
def process_packet(self, *args, **kwargs):
|
||||
packet = self._client.decode_packet(*args, **kwargs)
|
||||
thread = APRSDProcessPacketThread(packet=packet, config=self.config)
|
||||
thread.start()
|
||||
|
||||
|
||||
class APRSDProcessPacketThread(APRSDThread):
|
||||
|
||||
def __init__(self, packet, config, transport="aprsis"):
|
||||
def __init__(self, packet, config):
|
||||
self.packet = packet
|
||||
self.config = config
|
||||
self.transport = transport
|
||||
name = self.packet["raw"][:10]
|
||||
super().__init__(f"RX_PACKET-{name}")
|
||||
|
||||
@@ -254,7 +255,6 @@ class APRSDProcessPacketThread(APRSDThread):
|
||||
self.config["aprs"]["login"],
|
||||
fromcall,
|
||||
msg_id=msg_id,
|
||||
transport=self.transport,
|
||||
)
|
||||
ack.send()
|
||||
|
||||
@@ -275,7 +275,6 @@ class APRSDProcessPacketThread(APRSDThread):
|
||||
self.config["aprs"]["login"],
|
||||
fromcall,
|
||||
subreply,
|
||||
transport=self.transport,
|
||||
)
|
||||
msg.send()
|
||||
elif isinstance(reply, messaging.Message):
|
||||
@@ -296,20 +295,17 @@ class APRSDProcessPacketThread(APRSDThread):
|
||||
self.config["aprs"]["login"],
|
||||
fromcall,
|
||||
reply,
|
||||
transport=self.transport,
|
||||
)
|
||||
msg.send()
|
||||
|
||||
# If the message was for us and we didn't have a
|
||||
# response, then we send a usage statement.
|
||||
if tocall == self.config["aprs"]["login"] and not replied:
|
||||
reply = "Usage: weather, locate [call], time, fortune, ping"
|
||||
|
||||
LOG.warning("Sending help!")
|
||||
msg = messaging.TextMessage(
|
||||
self.config["aprs"]["login"],
|
||||
fromcall,
|
||||
reply,
|
||||
transport=self.transport,
|
||||
"Unknown command! Send 'help' message for help",
|
||||
)
|
||||
msg.send()
|
||||
except Exception as ex:
|
||||
@@ -321,88 +317,7 @@ class APRSDProcessPacketThread(APRSDThread):
|
||||
self.config["aprs"]["login"],
|
||||
fromcall,
|
||||
reply,
|
||||
transport=self.transport,
|
||||
)
|
||||
msg.send()
|
||||
|
||||
LOG.debug("Packet processing complete")
|
||||
|
||||
|
||||
class APRSDTXThread(APRSDThread):
|
||||
def __init__(self, msg_queues, config):
|
||||
super().__init__("TX_MSG")
|
||||
self.msg_queues = msg_queues
|
||||
self.config = config
|
||||
|
||||
def loop(self):
|
||||
try:
|
||||
msg = self.msg_queues["tx"].get(timeout=1)
|
||||
msg.send()
|
||||
except queue.Empty:
|
||||
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
|
||||
|
||||
-348
@@ -3,128 +3,13 @@
|
||||
import collections
|
||||
import errno
|
||||
import functools
|
||||
import logging
|
||||
import os
|
||||
from pathlib import Path
|
||||
import re
|
||||
import sys
|
||||
import threading
|
||||
|
||||
import click
|
||||
import update_checker
|
||||
import yaml
|
||||
|
||||
import aprsd
|
||||
from aprsd import plugin
|
||||
|
||||
|
||||
LOG_LEVELS = {
|
||||
"CRITICAL": logging.CRITICAL,
|
||||
"ERROR": logging.ERROR,
|
||||
"WARNING": logging.WARNING,
|
||||
"INFO": logging.INFO,
|
||||
"DEBUG": logging.DEBUG,
|
||||
}
|
||||
|
||||
DEFAULT_DATE_FORMAT = "%m/%d/%Y %I:%M:%S %p"
|
||||
DEFAULT_LOG_FORMAT = (
|
||||
"[%(asctime)s] [%(threadName)-20.20s] [%(levelname)-5.5s]"
|
||||
" %(message)s - [%(pathname)s:%(lineno)d]"
|
||||
)
|
||||
|
||||
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": {
|
||||
"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,
|
||||
"dateformat": DEFAULT_DATE_FORMAT,
|
||||
"trace": False,
|
||||
"enabled_plugins": plugin.CORE_MESSAGE_PLUGINS,
|
||||
"units": "imperial",
|
||||
"watch_list": {
|
||||
"enabled": False,
|
||||
# Who gets the alert?
|
||||
"alert_callsign": "NOCALL",
|
||||
# 43200 is 12 hours
|
||||
"alert_time_seconds": 43200,
|
||||
# How many packets to save in a ring Buffer
|
||||
# for a particular callsign
|
||||
"packet_keep_count": 10,
|
||||
"callsigns": [],
|
||||
"enabled_plugins": plugin.CORE_NOTIFY_PLUGINS,
|
||||
},
|
||||
"web": {
|
||||
"enabled": True,
|
||||
"logging_enabled": True,
|
||||
"host": "0.0.0.0",
|
||||
"port": 8001,
|
||||
"users": {
|
||||
"admin": "password-here",
|
||||
},
|
||||
},
|
||||
"email": {
|
||||
"enabled": True,
|
||||
"shortcuts": {
|
||||
"aa": "5551239999@vtext.com",
|
||||
"cl": "craiglamparter@somedomain.org",
|
||||
"wb": "555309@vtext.com",
|
||||
},
|
||||
"smtp": {
|
||||
"login": "SMTP_USERNAME",
|
||||
"password": "SMTP_PASSWORD",
|
||||
"host": "smtp.gmail.com",
|
||||
"port": 465,
|
||||
"use_ssl": False,
|
||||
"debug": False,
|
||||
},
|
||||
"imap": {
|
||||
"login": "IMAP_USERNAME",
|
||||
"password": "IMAP_PASSWORD",
|
||||
"host": "imap.gmail.com",
|
||||
"port": 993,
|
||||
"use_ssl": True,
|
||||
"debug": False,
|
||||
},
|
||||
},
|
||||
},
|
||||
"services": {
|
||||
"aprs.fi": {"apiKey": "APIKEYVALUE"},
|
||||
"openweathermap": {"apiKey": "APIKEYVALUE"},
|
||||
"opencagedata": {"apiKey": "APIKEYVALUE"},
|
||||
"avwx": {"base_url": "http://host:port", "apiKey": "APIKEYVALUE"},
|
||||
},
|
||||
}
|
||||
|
||||
home = str(Path.home())
|
||||
DEFAULT_CONFIG_DIR = f"{home}/.config/aprsd/"
|
||||
DEFAULT_SAVE_FILE = f"{home}/.config/aprsd/aprsd.p"
|
||||
DEFAULT_CONFIG_FILE = f"{home}/.config/aprsd/aprsd.yml"
|
||||
|
||||
|
||||
def synchronized(wrapped):
|
||||
@@ -175,239 +60,6 @@ def end_substr(original, substr):
|
||||
return idx
|
||||
|
||||
|
||||
def dump_default_cfg():
|
||||
return add_config_comments(
|
||||
yaml.dump(
|
||||
DEFAULT_CONFIG_DICT,
|
||||
indent=4,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def add_config_comments(raw_yaml):
|
||||
end_idx = end_substr(raw_yaml, "aprs:")
|
||||
if end_idx != -1:
|
||||
# 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,
|
||||
)
|
||||
|
||||
end_idx = end_substr(raw_yaml, "aprs.fi:")
|
||||
if end_idx != -1:
|
||||
# lets insert a comment
|
||||
raw_yaml = insert_str(
|
||||
raw_yaml,
|
||||
"\n # Get the apiKey from your aprs.fi account here: "
|
||||
"\n # http://aprs.fi/account",
|
||||
end_idx,
|
||||
)
|
||||
|
||||
end_idx = end_substr(raw_yaml, "opencagedata:")
|
||||
if end_idx != -1:
|
||||
# lets insert a comment
|
||||
raw_yaml = insert_str(
|
||||
raw_yaml,
|
||||
"\n # (Optional for TimeOpenCageDataPlugin) "
|
||||
"\n # Get the apiKey from your opencagedata account here: "
|
||||
"\n # https://opencagedata.com/dashboard#api-keys",
|
||||
end_idx,
|
||||
)
|
||||
|
||||
end_idx = end_substr(raw_yaml, "openweathermap:")
|
||||
if end_idx != -1:
|
||||
# lets insert a comment
|
||||
raw_yaml = insert_str(
|
||||
raw_yaml,
|
||||
"\n # (Optional for OWMWeatherPlugin) "
|
||||
"\n # Get the apiKey from your "
|
||||
"\n # openweathermap account here: "
|
||||
"\n # https://home.openweathermap.org/api_keys",
|
||||
end_idx,
|
||||
)
|
||||
|
||||
end_idx = end_substr(raw_yaml, "avwx:")
|
||||
if end_idx != -1:
|
||||
# lets insert a comment
|
||||
raw_yaml = insert_str(
|
||||
raw_yaml,
|
||||
"\n # (Optional for AVWXWeatherPlugin) "
|
||||
"\n # Use hosted avwx-api here: https://avwx.rest "
|
||||
"\n # or deploy your own from here: "
|
||||
"\n # https://github.com/avwx-rest/avwx-api",
|
||||
end_idx,
|
||||
)
|
||||
|
||||
return raw_yaml
|
||||
|
||||
|
||||
def create_default_config():
|
||||
"""Create a default config file."""
|
||||
# make sure the directory location exists
|
||||
config_file_expanded = os.path.expanduser(DEFAULT_CONFIG_FILE)
|
||||
config_dir = os.path.dirname(config_file_expanded)
|
||||
if not os.path.exists(config_dir):
|
||||
click.echo(f"Config dir '{config_dir}' doesn't exist, creating.")
|
||||
mkdir_p(config_dir)
|
||||
with open(config_file_expanded, "w+") as cf:
|
||||
cf.write(dump_default_cfg())
|
||||
|
||||
|
||||
def get_config(config_file):
|
||||
"""This tries to read the yaml config from <config_file>."""
|
||||
config_file_expanded = os.path.expanduser(config_file)
|
||||
if os.path.exists(config_file_expanded):
|
||||
with open(config_file_expanded) as stream:
|
||||
config = yaml.load(stream, Loader=yaml.FullLoader)
|
||||
return config
|
||||
else:
|
||||
if config_file == DEFAULT_CONFIG_FILE:
|
||||
click.echo(
|
||||
f"{config_file_expanded} is missing, creating config file",
|
||||
)
|
||||
create_default_config()
|
||||
msg = (
|
||||
"Default config file created at {}. Please edit with your "
|
||||
"settings.".format(config_file)
|
||||
)
|
||||
click.echo(msg)
|
||||
else:
|
||||
# The user provided a config file path different from the
|
||||
# Default, so we won't try and create it, just bitch and bail.
|
||||
msg = f"Custom config file '{config_file}' is missing."
|
||||
click.echo(msg)
|
||||
|
||||
sys.exit(-1)
|
||||
|
||||
|
||||
def conf_option_exists(conf, chain):
|
||||
_key = chain.pop(0)
|
||||
if _key in conf:
|
||||
return conf_option_exists(conf[_key], chain) if chain else conf[_key]
|
||||
|
||||
|
||||
def check_config_option(config, chain, default_fail=None):
|
||||
result = conf_option_exists(config, chain.copy())
|
||||
if result is None:
|
||||
raise Exception(
|
||||
"'{}' was not in config file".format(
|
||||
chain,
|
||||
),
|
||||
)
|
||||
else:
|
||||
if default_fail:
|
||||
if result == default_fail:
|
||||
# We have to fail and bail if the user hasn't edited
|
||||
# this config option.
|
||||
raise Exception(
|
||||
"Config file needs to be edited from provided defaults for {}.".format(
|
||||
chain,
|
||||
),
|
||||
)
|
||||
else:
|
||||
return config
|
||||
|
||||
|
||||
# This method tries to parse the config yaml file
|
||||
# and consume the settings.
|
||||
# If the required params don't exist,
|
||||
# it will look in the environment
|
||||
def parse_config(config_file):
|
||||
# for now we still use globals....ugh
|
||||
global CONFIG
|
||||
|
||||
def fail(msg):
|
||||
click.echo(msg)
|
||||
sys.exit(-1)
|
||||
|
||||
def check_option(config, chain, default_fail=None):
|
||||
try:
|
||||
config = check_config_option(config, chain, default_fail=default_fail)
|
||||
except Exception as ex:
|
||||
fail(repr(ex))
|
||||
else:
|
||||
return config
|
||||
|
||||
config = get_config(config_file)
|
||||
|
||||
# special check here to make sure user has edited the config file
|
||||
# and changed the ham callsign
|
||||
check_option(
|
||||
config,
|
||||
[
|
||||
"ham",
|
||||
"callsign",
|
||||
],
|
||||
default_fail=DEFAULT_CONFIG_DICT["ham"]["callsign"],
|
||||
)
|
||||
check_option(
|
||||
config,
|
||||
["services", "aprs.fi", "apiKey"],
|
||||
default_fail=DEFAULT_CONFIG_DICT["services"]["aprs.fi"]["apiKey"],
|
||||
)
|
||||
check_option(
|
||||
config,
|
||||
["aprs", "login"],
|
||||
default_fail=DEFAULT_CONFIG_DICT["aprs"]["login"],
|
||||
)
|
||||
check_option(
|
||||
config,
|
||||
["aprs", "password"],
|
||||
default_fail=DEFAULT_CONFIG_DICT["aprs"]["password"],
|
||||
)
|
||||
|
||||
# Ensure they change the admin password
|
||||
if config["aprsd"]["web"]["enabled"] is True:
|
||||
check_option(
|
||||
config,
|
||||
["aprsd", "web", "users", "admin"],
|
||||
default_fail=DEFAULT_CONFIG_DICT["aprsd"]["web"]["users"]["admin"],
|
||||
)
|
||||
|
||||
if config["aprsd"]["watch_list"]["enabled"] is True:
|
||||
check_option(
|
||||
config,
|
||||
["aprsd", "watch_list", "alert_callsign"],
|
||||
default_fail=DEFAULT_CONFIG_DICT["aprsd"]["watch_list"]["alert_callsign"],
|
||||
)
|
||||
|
||||
if config["aprsd"]["email"]["enabled"] is True:
|
||||
# Check IMAP server settings
|
||||
check_option(config, ["aprsd", "email", "imap", "host"])
|
||||
check_option(config, ["aprsd", "email", "imap", "port"])
|
||||
check_option(
|
||||
config,
|
||||
["aprsd", "email", "imap", "login"],
|
||||
default_fail=DEFAULT_CONFIG_DICT["aprsd"]["email"]["imap"]["login"],
|
||||
)
|
||||
check_option(
|
||||
config,
|
||||
["aprsd", "email", "imap", "password"],
|
||||
default_fail=DEFAULT_CONFIG_DICT["aprsd"]["email"]["imap"]["password"],
|
||||
)
|
||||
|
||||
# Check SMTP server settings
|
||||
check_option(config, ["aprsd", "email", "smtp", "host"])
|
||||
check_option(config, ["aprsd", "email", "smtp", "port"])
|
||||
check_option(
|
||||
config,
|
||||
["aprsd", "email", "smtp", "login"],
|
||||
default_fail=DEFAULT_CONFIG_DICT["aprsd"]["email"]["smtp"]["login"],
|
||||
)
|
||||
check_option(
|
||||
config,
|
||||
["aprsd", "email", "smtp", "password"],
|
||||
default_fail=DEFAULT_CONFIG_DICT["aprsd"]["email"]["smtp"]["password"],
|
||||
)
|
||||
|
||||
return config
|
||||
|
||||
|
||||
def human_size(bytes, units=None):
|
||||
"""Returns a human readable string representation of bytes"""
|
||||
if not units:
|
||||
|
||||
@@ -58,11 +58,31 @@ function update_watchlist_from_packet(callsign, val) {
|
||||
//console.log(watchlist)
|
||||
}
|
||||
|
||||
function update_seenlist( data ) {
|
||||
var seendiv = $("#seenDiv");
|
||||
var html_str = '<table class="ui celled striped table">'
|
||||
html_str += '<thead><tr><th>HAM Callsign</th><th>Age since last seen by APRSD</th>'
|
||||
html_str += '<th>Number of packets RX</th></tr></thead><tbody>'
|
||||
seendiv.html('')
|
||||
var seen_list = data["stats"]["aprsd"]["seen_list"]
|
||||
var len = Object.keys(seen_list).length
|
||||
$('#seen_count').html(len)
|
||||
jQuery.each(seen_list, function(i, val) {
|
||||
html_str += '<tr><td class="collapsing">'
|
||||
html_str += '<img id="callsign_'+i+'" class="aprsd_1"></img>' + i + '</td>'
|
||||
html_str += '<td>' + val["last"] + '</td>'
|
||||
html_str += '<td>' + val["count"] + '</td></tr>'
|
||||
});
|
||||
html_str += "</tbody></table>";
|
||||
seendiv.append(html_str);
|
||||
}
|
||||
|
||||
function update_plugins( data ) {
|
||||
var plugindiv = $("#pluginDiv");
|
||||
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 += '<th>Version</th>'
|
||||
html_str += '</tr></thead><tbody>'
|
||||
plugindiv.html('')
|
||||
|
||||
@@ -72,7 +92,9 @@ 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["enabled"] + '</td><td>' + val["rx"] + '</td><td>' + val["tx"] + '</td></tr>';
|
||||
html_str += '<tr><td class="collapsing">' + key + '</td>';
|
||||
html_str += '<td>' + val["enabled"] + '</td><td>' + val["rx"] + '</td>';
|
||||
html_str += '<td>' + val["tx"] + '</td><td>' + val["version"] +'</td></tr>';
|
||||
}
|
||||
html_str += "</tbody></table>";
|
||||
plugindiv.append(html_str);
|
||||
@@ -140,6 +162,7 @@ function start_update() {
|
||||
success: function(data) {
|
||||
update_stats(data);
|
||||
update_watchlist(data);
|
||||
update_seenlist(data);
|
||||
update_plugins(data);
|
||||
},
|
||||
complete: function() {
|
||||
|
||||
@@ -18,7 +18,6 @@
|
||||
<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>
|
||||
|
||||
|
||||
@@ -79,6 +78,7 @@
|
||||
<div class="ui top attached tabular menu">
|
||||
<div class="active item" data-tab="charts-tab">Charts</div>
|
||||
<div class="item" data-tab="msgs-tab">Messages</div>
|
||||
<div class="item" data-tab="seen-tab">Seen List</div>
|
||||
<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>
|
||||
@@ -132,6 +132,13 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="ui bottom attached tab segment" data-tab="seen-tab">
|
||||
<h3 class="ui dividing header">
|
||||
Callsign Seen List (<span id="seen_count">{{ seen_count }}</span>)
|
||||
</h3>
|
||||
<div id="seenDiv" class="ui mini text">Loading</div>
|
||||
</div>
|
||||
|
||||
<div class="ui bottom attached tab segment" data-tab="watch-tab">
|
||||
<h3 class="ui dividing header">
|
||||
Callsign Watch List (<span id="watch_count">{{ watch_count }}</span>)
|
||||
|
||||
+9
-10
@@ -1,4 +1,5 @@
|
||||
FROM python:3-bullseye as aprsd
|
||||
#FROM python:3-bullseye as aprsd
|
||||
FROM ubuntu:focal as aprsd
|
||||
|
||||
# Dockerfile for building a container during aprsd development.
|
||||
|
||||
@@ -13,22 +14,17 @@ ENV GID=${GID:-1000}
|
||||
ENV LC_ALL=C.UTF-8
|
||||
ENV LANG=C.UTF-8
|
||||
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
RUN apt update
|
||||
RUN apt install -y git build-essential
|
||||
RUN apt install -y libffi-dev python3-dev libssl-dev
|
||||
#RUN apt-get install -y apt-utils
|
||||
#RUN apt-get install -y pkg-config
|
||||
#RUN apt upgrade -y
|
||||
#RUN apk add --update git wget bash
|
||||
#RUN apk add --update gcc linux-headers musl-dev libffi-dev libc-dev
|
||||
#RUN apk add --update openssl-dev
|
||||
#RUN add cmd:pip3 lsb-release
|
||||
RUN apt install -y libffi-dev python3-dev libssl-dev libxml2-dev libxslt-dev
|
||||
RUN apt install -y python3 python3-pip python3-dev python3-lxml
|
||||
|
||||
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==2.3.0
|
||||
RUN pip install aprsd
|
||||
|
||||
# Ensure /config is there with a default config file
|
||||
USER root
|
||||
@@ -43,3 +39,6 @@ VOLUME ["/config", "/plugins"]
|
||||
USER $APRS_USER
|
||||
ADD bin/run.sh /usr/local/bin
|
||||
ENTRYPOINT ["/usr/local/bin/run.sh"]
|
||||
|
||||
HEALTHCHECK --interval=5m --timeout=12s --start-period=30s \
|
||||
CMD aprsd healthcheck --config /config/aprsd.yml --url http://localhost:8001/stats
|
||||
|
||||
+20
-9
@@ -1,23 +1,27 @@
|
||||
FROM python:3.8-slim as aprsd
|
||||
# syntax=docker/dockerfile:1
|
||||
FROM ubuntu:focal as aprsd
|
||||
|
||||
# Dockerfile for building a container during aprsd development.
|
||||
ARG BRANCH
|
||||
ARG branch
|
||||
ARG UID
|
||||
ARG GID
|
||||
|
||||
ENV APRS_USER=aprs
|
||||
ENV HOME=/home/aprs
|
||||
ENV APRSD=http://github.com/craigerl/aprsd.git
|
||||
ENV APRSD_BRANCH=${BRANCH:-master}
|
||||
ENV APRSD_BRANCH=${branch:-master}
|
||||
ENV VIRTUAL_ENV=$HOME/.venv3
|
||||
ENV UID=${UID:-1000}
|
||||
ENV GID=${GID:-1000}
|
||||
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
ENV INSTALL=$HOME/install
|
||||
RUN apt update
|
||||
RUN apt install -y git build-essential
|
||||
RUN apt install -y libffi-dev python3-dev libssl-dev
|
||||
RUN apt install -y bash fortune
|
||||
RUN apt install -y --no-install-recommends git build-essential bash fortune
|
||||
RUN apt install -y libffi-dev python3-dev libssl-dev libxml2-dev libxslt-dev
|
||||
RUN apt install -y python3 python3-pip python3-dev python3-lxml
|
||||
#RUN apt-get clean
|
||||
RUN apt-get -o Dpkg::Options::="--force-confmiss" install --reinstall netbase
|
||||
|
||||
RUN addgroup --gid 1001 $APRS_USER
|
||||
RUN useradd -m -u $UID -g $APRS_USER $APRS_USER
|
||||
@@ -25,17 +29,21 @@ RUN useradd -m -u $UID -g $APRS_USER $APRS_USER
|
||||
ENV LC_ALL=C.UTF-8
|
||||
ENV LANG=C.UTF-8
|
||||
|
||||
WORKDIR $HOME
|
||||
USER $APRS_USER
|
||||
RUN pip3 install wheel
|
||||
RUN pip install wheel
|
||||
#RUN python3 -m venv $VIRTUAL_ENV
|
||||
ENV PATH="$VIRTUAL_ENV/bin:$PATH"
|
||||
#ENV PATH="$VIRTUAL_ENV/bin:$PATH"
|
||||
RUN echo "export PATH=\$PATH:\$HOME/.local/bin" >> $HOME/.bashrc
|
||||
RUN cat $HOME/.bashrc
|
||||
|
||||
USER root
|
||||
WORKDIR $HOME
|
||||
RUN mkdir $INSTALL
|
||||
RUN git clone -b $APRSD_BRANCH $APRSD $INSTALL/aprsd
|
||||
RUN cd $INSTALL/aprsd && pip3 install .
|
||||
RUN cd $INSTALL/aprsd && pip3 install -v .
|
||||
RUN ls -al /usr/local/bin
|
||||
RUN ls -al /usr/bin
|
||||
RUN which aprsd
|
||||
RUN mkdir -p /config
|
||||
RUN aprsd sample-config > /config/aprsd.yml
|
||||
@@ -48,3 +56,6 @@ VOLUME ["/config", "/plugins"]
|
||||
|
||||
ADD bin/run.sh $HOME/
|
||||
ENTRYPOINT ["/home/aprs/run.sh"]
|
||||
|
||||
HEALTHCHECK --interval=5m --timeout=12s --start-period=30s \
|
||||
CMD aprsd healthcheck --config /config/aprsd.yml --url http://localhost:8001/stats
|
||||
|
||||
+1
-1
@@ -20,4 +20,4 @@ if [ ! -e "$APRSD_CONFIG" ]; then
|
||||
aprsd sample-config > $APRSD_CONFIG
|
||||
fi
|
||||
|
||||
/usr/local/bin/aprsd server -c $APRSD_CONFIG --loglevel DEBUG
|
||||
exec aprsd server -c $APRSD_CONFIG --loglevel DEBUG
|
||||
|
||||
+29
-9
@@ -1,6 +1,10 @@
|
||||
#!/bin/bash
|
||||
# Official docker image build script.
|
||||
|
||||
# docker buildx create --name multiarch \
|
||||
# --platform linux/arm/v7,linux/arm/v6,linux/arm64,linux/amd64 \
|
||||
# --config ./buildkit.toml --use --driver-opt image=moby/buildkit:master
|
||||
|
||||
|
||||
usage() {
|
||||
cat << EOF
|
||||
@@ -15,14 +19,18 @@ EOF
|
||||
|
||||
ALL_PLATFORMS=0
|
||||
DEV=0
|
||||
TAG="master"
|
||||
TAG="latest"
|
||||
BRANCH="master"
|
||||
|
||||
while getopts “t:da” OPTION
|
||||
while getopts “t:dab:” OPTION
|
||||
do
|
||||
case $OPTION in
|
||||
t)
|
||||
TAG=$OPTARG
|
||||
;;
|
||||
b)
|
||||
BRANCH=$OPTARG
|
||||
;;
|
||||
a)
|
||||
ALL_PLATFORMS=1
|
||||
;;
|
||||
@@ -36,29 +44,41 @@ do
|
||||
esac
|
||||
done
|
||||
|
||||
VERSION="2.2.1"
|
||||
VERSION="2.4.2"
|
||||
|
||||
if [ $ALL_PLATFORMS -eq 1 ]
|
||||
then
|
||||
PLATFORMS="linux/arm/v7,linux/arm/v6,linux/arm64,linux/amd64"
|
||||
PLATFORMS="linux/arm/v7,linux/arm64,linux/amd64"
|
||||
#PLATFORMS="linux/arm/v7,linux/arm/v6,linux/amd64"
|
||||
else
|
||||
PLATFORMS="linux/amd64"
|
||||
fi
|
||||
|
||||
echo "Build with tag=${TAG} BRANCH=${BRANCH} dev?=${DEV} platforms?=${PLATFORMS}"
|
||||
|
||||
|
||||
echo "Destroying old multiarch build container"
|
||||
docker buildx rm multiarch
|
||||
docker run --rm --privileged multiarch/qemu-user-static --reset -p yes
|
||||
echo "Creating new buildx container"
|
||||
docker buildx create --name multiarch --driver docker-container --use --platform linux/arm/v7,linux/arm/v6,linux/arm64,linux/amd64 --config ./buildkit.toml --use --driver-opt image=moby/buildkit:master
|
||||
docker buildx inspect --bootstrap
|
||||
|
||||
if [ $DEV -eq 1 ]
|
||||
then
|
||||
echo "Build -DEV- with tag=${TAG} BRANCH=${BRANCH} platforms?=${PLATFORMS}"
|
||||
# Use this script to locally build the docker image
|
||||
docker buildx build --push --platform $PLATFORMS \
|
||||
-t harbor.hemna.com/hemna6969/aprsd:$TAG \
|
||||
-f Dockerfile-dev --no-cache .
|
||||
-f Dockerfile-dev --build-arg branch=$BRANCH --no-cache .
|
||||
else
|
||||
# Use this script to locally build the docker image
|
||||
echo "Build with tag=${TAG} BRANCH=${BRANCH} platforms?=${PLATFORMS}"
|
||||
docker buildx build --push --platform $PLATFORMS \
|
||||
--allow security.insecure \
|
||||
-t hemna6969/aprsd:$VERSION \
|
||||
-t hemna6969/aprsd:latest \
|
||||
-t harbor.hemna.com/hemna6969/aprsd:latest \
|
||||
-t hemna6969/aprsd:$TAG \
|
||||
-t harbor.hemna.com/hemna6969/aprsd:$TAG \
|
||||
-t harbor.hemna.com/hemna6969/aprsd:$VERSION \
|
||||
-f Dockerfile .
|
||||
|
||||
|
||||
fi
|
||||
|
||||
+1
-1
@@ -17,7 +17,7 @@ pytz
|
||||
requests
|
||||
six
|
||||
thesmuggler
|
||||
yfinance
|
||||
update_checker
|
||||
flask-socketio
|
||||
eventlet
|
||||
tabulate
|
||||
|
||||
+3
-19
@@ -63,20 +63,10 @@ jinja2==3.0.1
|
||||
# via
|
||||
# click-completion
|
||||
# flask
|
||||
lxml==4.6.3
|
||||
# via yfinance
|
||||
markupsafe==2.0.1
|
||||
# via jinja2
|
||||
multitasking==0.0.9
|
||||
# via yfinance
|
||||
numpy==1.21.2
|
||||
# via
|
||||
# pandas
|
||||
# yfinance
|
||||
opencage==2.0.0
|
||||
# via -r requirements.in
|
||||
pandas==1.3.2
|
||||
# via yfinance
|
||||
pbr==5.6.0
|
||||
# via -r requirements.in
|
||||
pluggy==1.0.0
|
||||
@@ -89,16 +79,12 @@ 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
|
||||
# via -r requirements.in
|
||||
pyyaml==5.4.1
|
||||
# via -r requirements.in
|
||||
requests==2.26.0
|
||||
@@ -106,7 +92,6 @@ requests==2.26.0
|
||||
# -r requirements.in
|
||||
# opencage
|
||||
# update-checker
|
||||
# yfinance
|
||||
shellingham==1.4.0
|
||||
# via click-completion
|
||||
signalslot==0.1.2
|
||||
@@ -118,8 +103,9 @@ six==1.16.0
|
||||
# eventlet
|
||||
# imapclient
|
||||
# pyopenssl
|
||||
# python-dateutil
|
||||
# signalslot
|
||||
tabulate==0.8.9
|
||||
# via -r requirements.in
|
||||
thesmuggler==1.0.1
|
||||
# via -r requirements.in
|
||||
update-checker==0.18.0
|
||||
@@ -130,5 +116,3 @@ weakrefmethod==1.0.3
|
||||
# via signalslot
|
||||
werkzeug==2.0.0
|
||||
# via flask
|
||||
yfinance==0.1.63
|
||||
# via -r requirements.in
|
||||
|
||||
@@ -33,11 +33,7 @@ packages =
|
||||
|
||||
[entry_points]
|
||||
console_scripts =
|
||||
aprsd = aprsd.main:main
|
||||
aprsd-listen = aprsd.listen:main
|
||||
aprsd-dev = aprsd.dev:main
|
||||
aprsd-healthcheck = aprsd.healthcheck:main
|
||||
fake_aprs = aprsd.fake_aprs:main
|
||||
aprsd = aprsd.aprsd:main
|
||||
|
||||
[build_sphinx]
|
||||
source-dir = docs
|
||||
|
||||
+8
-8
@@ -5,21 +5,21 @@ from aprsd.plugins import email
|
||||
|
||||
class TestEmail(unittest.TestCase):
|
||||
def test_get_email_from_shortcut(self):
|
||||
email.CONFIG = {"aprsd": {"email": {"shortcuts": {}}}}
|
||||
config = {"aprsd": {"email": {"shortcuts": {}}}}
|
||||
email_address = "something@something.com"
|
||||
addr = f"-{email_address}"
|
||||
actual = email.get_email_from_shortcut(addr)
|
||||
actual = email.get_email_from_shortcut(config, addr)
|
||||
self.assertEqual(addr, actual)
|
||||
|
||||
email.CONFIG = {"aprsd": {"email": {"nothing": "nothing"}}}
|
||||
actual = email.get_email_from_shortcut(addr)
|
||||
config = {"aprsd": {"email": {"nothing": "nothing"}}}
|
||||
actual = email.get_email_from_shortcut(config, addr)
|
||||
self.assertEqual(addr, actual)
|
||||
|
||||
email.CONFIG = {"aprsd": {"email": {"shortcuts": {"not_used": "empty"}}}}
|
||||
actual = email.get_email_from_shortcut(addr)
|
||||
config = {"aprsd": {"email": {"shortcuts": {"not_used": "empty"}}}}
|
||||
actual = email.get_email_from_shortcut(config, addr)
|
||||
self.assertEqual(addr, actual)
|
||||
|
||||
email.CONFIG = {"aprsd": {"email": {"shortcuts": {"-wb": email_address}}}}
|
||||
config = {"aprsd": {"email": {"shortcuts": {"-wb": email_address}}}}
|
||||
short = "-wb"
|
||||
actual = email.get_email_from_shortcut(short)
|
||||
actual = email.get_email_from_shortcut(config, short)
|
||||
self.assertEqual(email_address, actual)
|
||||
|
||||
@@ -6,9 +6,14 @@ from aprsd import messaging
|
||||
|
||||
|
||||
class TestMessageTrack(unittest.TestCase):
|
||||
|
||||
def setUp(self) -> None:
|
||||
config = {}
|
||||
messaging.MsgTrack(config=config)
|
||||
|
||||
def _clean_track(self):
|
||||
track = messaging.MsgTrack()
|
||||
track.track = {}
|
||||
track.data = {}
|
||||
track.total_messages_tracked = 0
|
||||
return track
|
||||
|
||||
|
||||
+11
-8
@@ -4,7 +4,7 @@ from unittest import mock
|
||||
import pytz
|
||||
|
||||
import aprsd
|
||||
from aprsd import messaging, packets, stats, utils
|
||||
from aprsd import config, messaging, packets, stats
|
||||
from aprsd.fuzzyclock import fuzzy
|
||||
from aprsd.plugins import fortune as fortune_plugin
|
||||
from aprsd.plugins import ping as ping_plugin
|
||||
@@ -19,11 +19,15 @@ class TestPlugin(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.fromcall = fake.FAKE_FROM_CALLSIGN
|
||||
self.ack = 1
|
||||
self.config = utils.DEFAULT_CONFIG_DICT
|
||||
self.config = config.DEFAULT_CONFIG_DICT
|
||||
self.config["ham"]["callsign"] = self.fromcall
|
||||
self.config["aprs"]["login"] = fake.FAKE_TO_CALLSIGN
|
||||
self.config["services"]["aprs.fi"]["apiKey"] = "something"
|
||||
# Inintialize the stats object with the config
|
||||
stats.APRSDStats(self.config)
|
||||
packets.WatchList(config=self.config)
|
||||
packets.SeenList(config=self.config)
|
||||
messaging.MsgTrack(config=self.config)
|
||||
|
||||
@mock.patch.object(fake.FakeBaseNoThreadsPlugin, "process")
|
||||
def test_base_plugin_no_threads(self, mock_process):
|
||||
@@ -124,9 +128,9 @@ class TestPlugin(unittest.TestCase):
|
||||
class TestFortunePlugin(TestPlugin):
|
||||
@mock.patch("shutil.which")
|
||||
def test_fortune_fail(self, mock_which):
|
||||
fortune = fortune_plugin.FortunePlugin(self.config)
|
||||
mock_which.return_value = None
|
||||
expected = "Fortune command not installed"
|
||||
fortune = fortune_plugin.FortunePlugin(self.config)
|
||||
expected = "FortunePlugin isn't enabled"
|
||||
packet = fake.fake_packet(message="fortune")
|
||||
actual = fortune.filter(packet)
|
||||
self.assertEqual(expected, actual)
|
||||
@@ -134,10 +138,9 @@ class TestFortunePlugin(TestPlugin):
|
||||
@mock.patch("subprocess.check_output")
|
||||
@mock.patch("shutil.which")
|
||||
def test_fortune_success(self, mock_which, mock_output):
|
||||
fortune = fortune_plugin.FortunePlugin(self.config)
|
||||
mock_which.return_value = "/usr/bin/games"
|
||||
|
||||
mock_which.return_value = "/usr/bin/games/fortune"
|
||||
mock_output.return_value = "Funny fortune"
|
||||
fortune = fortune_plugin.FortunePlugin(self.config)
|
||||
|
||||
expected = "Funny fortune"
|
||||
packet = fake.fake_packet(message="fortune")
|
||||
@@ -159,7 +162,7 @@ class TestQueryPlugin(TestPlugin):
|
||||
@mock.patch("aprsd.messaging.MsgTrack.restart_delayed")
|
||||
def test_query_restart_delayed(self, mock_restart):
|
||||
track = messaging.MsgTrack()
|
||||
track.track = {}
|
||||
track.data = {}
|
||||
packet = fake.fake_packet(message="!4")
|
||||
query = query_plugin.QueryPlugin(self.config)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user