aprsd/aprsd/main.py

530 lines
14 KiB
Python
Raw Normal View History

2018-06-13 11:58:56 -04:00
#
# 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.
2018-06-13 11:58:56 -04:00
#
# APRS messages:
2018-11-21 14:29:15 -05:00
# l(ocation) = descriptive location of calling station
# w(eather) = temp, (hi/low) forecast, later forecast
# t(ime) = respond with the current time
2018-06-13 11:58:56 -04:00
# f(ortune) = respond with a short fortune
# -email_addr email text = send an email
# -2 = display the last 2 emails received
2018-11-21 14:29:15 -05:00
# p(ing) = respond with Pong!/time
2018-06-13 11:58:56 -04:00
# anything else = respond with usage
#
# (C)2018 Craig Lamparter
# License GPLv2
2017-10-31 12:58:06 -04:00
#
# python included libs
import datetime
import logging
2021-01-08 15:47:30 -05:00
from logging import NullHandler
from logging.handlers import RotatingFileHandler
2018-06-13 11:58:56 -04:00
import os
import signal
import sys
import time
import aprslib
from aprslib.exceptions import LoginError
import click
import click_completion
2021-01-08 15:47:30 -05:00
# local imports here
import aprsd
from aprsd import (
2021-08-30 13:34:25 -04:00
client, flask, kissclient, messaging, packets, plugin, stats, threads,
trace, utils,
)
2018-06-13 11:58:56 -04:00
# 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()
2018-11-21 14:29:15 -05:00
# 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"
# }
2018-06-13 11:58:56 -04:00
2020-12-09 08:54:17 -05:00
def custom_startswith(string, incomplete):
"""A custom completion match that supports case insensitive matching."""
if os.environ.get("_CLICK_COMPLETION_COMMAND_CASE_INSENSITIVE_COMPLETE"):
2020-12-09 08:54:17 -05:00
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())
)
2020-12-09 08:54:17 -05:00
@click.group(help=cmd_help, context_settings=CONTEXT_SETTINGS)
2020-12-09 08:54:17 -05:00
@click.version_option()
def main():
pass
@main.command()
@click.option(
2021-01-08 15:47:30 -05:00
"-i",
"--case-insensitive/--no-case-insensitive",
help="Case insensitive completion",
)
@click.argument(
"shell",
required=False,
type=click_completion.DocumentedChoice(click_completion.core.shells),
)
2020-12-09 08:54:17 -05:00
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 {}
)
2020-12-09 08:54:17 -05:00
click.echo(click_completion.core.get_code(shell, extra_env=extra_env))
@main.command()
@click.option(
2021-01-08 15:47:30 -05:00
"--append/--overwrite",
help="Append the completion code to the file",
default=None,
)
@click.option(
2021-01-08 15:47:30 -05:00
"-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)
2020-12-09 08:54:17 -05:00
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(
2021-01-08 15:47:30 -05:00
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)
2020-12-09 08:54:17 -05:00
if not quiet:
sh = logging.StreamHandler(sys.stdout)
sh.setFormatter(log_formatter)
LOG.addHandler(sh)
if imap_logger:
imap_logger.addHandler(sh)
2018-11-21 14:29:15 -05:00
@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(
2021-01-08 15:47:30 -05:00
["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(
2021-01-08 15:47:30 -05:00
loglevel,
quiet,
config_file,
aprs_login,
aprs_password,
no_ack,
raw,
2021-01-08 15:47:30 -05:00
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(
2021-01-08 15:47:30 -05:00
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)
# 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() ###
2020-12-09 08:54:17 -05:00
@main.command()
@click.option(
"--loglevel",
default="INFO",
show_default=True,
type=click.Choice(
2021-01-08 15:47:30 -05:00
["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,
):
2020-12-09 08:54:17 -05:00
"""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)
# Create the initial PM singleton and Register plugins
plugin_manager = plugin.PluginManager(config)
plugin_manager.setup_plugins()
Added the ability to use direwolf KISS socket This patch adds APRS KISS connectivity. I have tested this with a running Direwolf install via either a serial KISS connection or the optional new TCPKISS connection, both to Direwolf. This adds the new required aioax25 python library for the underlying KISS and AX25 support. NOTE: For the TCPKISS connection, this patch requires a pull request patch the aioax25 library to include a TCP Based KISS TNC client to enable the TCPKISS client So you will need to pull down this PR https://github.com/sjlongland/aioax25/pull/7 To enable this, Edit your aprsd.yml file and enable one of the 2 KISS connections. Only one is supported at a time. kiss: serial: enabled: True device: /dev/ttyS1 baudrate: 9600 or kiss: tcp: enabled: True host: "ip address/hostname of direwolf" port: "direwolf configured kiss port" This patch alters the Message object classes to be able to send messages out via the aprslib socket connection to the APRS-IS network on the internet, or via the direwolf KISS TCP socket, depending on the origination of the initial message coming in. If an APRS message comes in via APRS-IS, then replies will go out APRS-IS. IF an APRS message comes in via direwolf, then replies will go out via direwolf KISS TCP socket. Both can work at the same time. TODO: I need some real APRS message packets to verify that the new thread is processing packets correctly through the plugins and able to send the resulting messages back out to direwolf. Have a hard coded callsign for now in the kissclient consumer call, just so I can see messages coming in from direwolf. I dont' have an APRS capable radio at the moment to send messages directly to direwolf. Might need to write a simple python socket server to send fake APRS messages to aprsd kiss, just for finishing up development.
2021-02-25 21:01:52 -05:00
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()
Added the ability to use direwolf KISS socket This patch adds APRS KISS connectivity. I have tested this with a running Direwolf install via either a serial KISS connection or the optional new TCPKISS connection, both to Direwolf. This adds the new required aioax25 python library for the underlying KISS and AX25 support. NOTE: For the TCPKISS connection, this patch requires a pull request patch the aioax25 library to include a TCP Based KISS TNC client to enable the TCPKISS client So you will need to pull down this PR https://github.com/sjlongland/aioax25/pull/7 To enable this, Edit your aprsd.yml file and enable one of the 2 KISS connections. Only one is supported at a time. kiss: serial: enabled: True device: /dev/ttyS1 baudrate: 9600 or kiss: tcp: enabled: True host: "ip address/hostname of direwolf" port: "direwolf configured kiss port" This patch alters the Message object classes to be able to send messages out via the aprslib socket connection to the APRS-IS network on the internet, or via the direwolf KISS TCP socket, depending on the origination of the initial message coming in. If an APRS message comes in via APRS-IS, then replies will go out APRS-IS. IF an APRS message comes in via direwolf, then replies will go out via direwolf KISS TCP socket. Both can work at the same time. TODO: I need some real APRS message packets to verify that the new thread is processing packets correctly through the plugins and able to send the resulting messages back out to direwolf. Have a hard coded callsign for now in the kissclient consumer call, just so I can see messages coming in from direwolf. I dont' have an APRS capable radio at the moment to send messages directly to direwolf. Might need to write a simple python socket server to send fake APRS messages to aprsd kiss, just for finishing up development.
2021-02-25 21:01:52 -05:00
else:
LOG.info(
"APRS network connection Not Enabled in config. This is"
" for setups without internet connectivity.",
)
# 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)
Added the ability to use direwolf KISS socket This patch adds APRS KISS connectivity. I have tested this with a running Direwolf install via either a serial KISS connection or the optional new TCPKISS connection, both to Direwolf. This adds the new required aioax25 python library for the underlying KISS and AX25 support. NOTE: For the TCPKISS connection, this patch requires a pull request patch the aioax25 library to include a TCP Based KISS TNC client to enable the TCPKISS client So you will need to pull down this PR https://github.com/sjlongland/aioax25/pull/7 To enable this, Edit your aprsd.yml file and enable one of the 2 KISS connections. Only one is supported at a time. kiss: serial: enabled: True device: /dev/ttyS1 baudrate: 9600 or kiss: tcp: enabled: True host: "ip address/hostname of direwolf" port: "direwolf configured kiss port" This patch alters the Message object classes to be able to send messages out via the aprslib socket connection to the APRS-IS network on the internet, or via the direwolf KISS TCP socket, depending on the origination of the initial message coming in. If an APRS message comes in via APRS-IS, then replies will go out APRS-IS. IF an APRS message comes in via direwolf, then replies will go out via direwolf KISS TCP socket. Both can work at the same time. TODO: I need some real APRS message packets to verify that the new thread is processing packets correctly through the plugins and able to send the resulting messages back out to direwolf. Have a hard coded callsign for now in the kissclient consumer call, just so I can see messages coming in from direwolf. I dont' have an APRS capable radio at the moment to send messages directly to direwolf. Might need to write a simple python socket server to send fake APRS messages to aprsd kiss, just for finishing up development.
2021-02-25 21:01:52 -05:00
if kissclient.KISSClient.kiss_enabled(config):
kcl = kissclient.KISSClient(config=config)
2021-08-30 13:34:25 -04:00
# This initializes the client object.
Added the ability to use direwolf KISS socket This patch adds APRS KISS connectivity. I have tested this with a running Direwolf install via either a serial KISS connection or the optional new TCPKISS connection, both to Direwolf. This adds the new required aioax25 python library for the underlying KISS and AX25 support. NOTE: For the TCPKISS connection, this patch requires a pull request patch the aioax25 library to include a TCP Based KISS TNC client to enable the TCPKISS client So you will need to pull down this PR https://github.com/sjlongland/aioax25/pull/7 To enable this, Edit your aprsd.yml file and enable one of the 2 KISS connections. Only one is supported at a time. kiss: serial: enabled: True device: /dev/ttyS1 baudrate: 9600 or kiss: tcp: enabled: True host: "ip address/hostname of direwolf" port: "direwolf configured kiss port" This patch alters the Message object classes to be able to send messages out via the aprslib socket connection to the APRS-IS network on the internet, or via the direwolf KISS TCP socket, depending on the origination of the initial message coming in. If an APRS message comes in via APRS-IS, then replies will go out APRS-IS. IF an APRS message comes in via direwolf, then replies will go out via direwolf KISS TCP socket. Both can work at the same time. TODO: I need some real APRS message packets to verify that the new thread is processing packets correctly through the plugins and able to send the resulting messages back out to direwolf. Have a hard coded callsign for now in the kissclient consumer call, just so I can see messages coming in from direwolf. I dont' have an APRS capable radio at the moment to send messages directly to direwolf. Might need to write a simple python socket server to send fake APRS messages to aprsd kiss, just for finishing up development.
2021-02-25 21:01:52 -05:00
kcl.client
2021-08-30 13:34:25 -04:00
kissrx_thread = threads.KISSRXThread(msg_queues=threads.msg_queues, config=config)
Added the ability to use direwolf KISS socket This patch adds APRS KISS connectivity. I have tested this with a running Direwolf install via either a serial KISS connection or the optional new TCPKISS connection, both to Direwolf. This adds the new required aioax25 python library for the underlying KISS and AX25 support. NOTE: For the TCPKISS connection, this patch requires a pull request patch the aioax25 library to include a TCP Based KISS TNC client to enable the TCPKISS client So you will need to pull down this PR https://github.com/sjlongland/aioax25/pull/7 To enable this, Edit your aprsd.yml file and enable one of the 2 KISS connections. Only one is supported at a time. kiss: serial: enabled: True device: /dev/ttyS1 baudrate: 9600 or kiss: tcp: enabled: True host: "ip address/hostname of direwolf" port: "direwolf configured kiss port" This patch alters the Message object classes to be able to send messages out via the aprslib socket connection to the APRS-IS network on the internet, or via the direwolf KISS TCP socket, depending on the origination of the initial message coming in. If an APRS message comes in via APRS-IS, then replies will go out APRS-IS. IF an APRS message comes in via direwolf, then replies will go out via direwolf KISS TCP socket. Both can work at the same time. TODO: I need some real APRS message packets to verify that the new thread is processing packets correctly through the plugins and able to send the resulting messages back out to direwolf. Have a hard coded callsign for now in the kissclient consumer call, just so I can see messages coming in from direwolf. I dont' have an APRS capable radio at the moment to send messages directly to direwolf. Might need to write a simple python socket server to send fake APRS messages to aprsd kiss, just for finishing up development.
2021-02-25 21:01:52 -05:00
kissrx_thread.start()
messaging.MsgTrack().restart()
keepalive = threads.KeepAliveThread(config=config)
keepalive.start()
try:
web_enabled = utils.check_config_option(config, ["aprsd", "web", "enabled"])
except Exception:
web_enabled = False
if web_enabled:
flask_enabled = True
(socketio, app) = flask.init_flask(config, loglevel, quiet)
socketio.run(
app,
2021-08-30 12:18:14 -04:00
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
2017-10-31 12:58:06 -04:00
if __name__ == "__main__":
2020-12-09 08:54:17 -05:00
main()