1
0
mirror of https://github.com/craigerl/aprsd.git synced 2026-08-18 17:44:02 -04:00

Compare commits

...

34 Commits

Author SHA1 Message Date
hemna 7292744a78 Restore previous conversations in webchat
This patch saves the webchat conversations messages in the browser's
local storage.  When the user comes back to the page, the
conversations are restored.
2023-09-05 14:14:52 -04:00
hemna 619b1b708e Remove VIM from Dockerfile
If you need vim, you can just ssh into the container and apt-get
install vim.
2023-09-05 08:01:42 -04:00
hemna 008b2ab09e recreate client during reset()
This patch re-creates the client object during a client.reset() call.
2023-09-01 16:11:19 -04:00
hemna 4b56e99689 updated github workflows 2023-09-01 14:51:55 -04:00
hemna 10bf04929e Updated documentation build 2023-09-01 14:43:29 -04:00
hemna a9e8050ae6 Removed admin_web.py
This patch removes the old admin_web.py.   Use the aprsd.wsgi
for the admin interface.
2023-09-01 14:38:55 -04:00
hemna 82f77b7a6a Removed some RPC server log noise 2023-08-28 09:19:54 -04:00
hemna 570fdb98a7 Fixed admin page packet date
The date timestamp was always showing as 1970.  Had to adjust
the javascript conversion from epoch to Date object
2023-08-28 09:18:50 -04:00
hemna 9582812041 RPC Server logs the client IP on failed auth
this patch adds an error log for the client IP of who connects
to the rpc server without proper auth key.
2023-08-23 13:48:09 -04:00
hemna 859f904602 Start keepalive thread first
This patch changes the order of the threads starting.  The Keepalive
thread's job is to test the aprsis/kiss client to see if it's up and
running, and then issue a reset if it's down.   On SIGINT, the keepalive
might issue that reset in the middle of a shutdown, which might cause
things to hang when everything should be shutting down.  Making the
KeepaliveThread first, means it will be the first to be shut down as
well, preventing the next loop from resetting the client.
2023-08-23 13:45:46 -04:00
hemna 34311f0fbd fixed an issue in the mobile webchat
The global socket var wasn't defined globally in send-message-mobile.js
2023-08-23 13:15:50 -04:00
hemna 2416f0ea1a Added dupe checkig code to webchat mobile 2023-08-22 16:03:27 -04:00
hemna 377842c2ec click on the div after added. 2023-08-22 13:46:43 -04:00
hemna a8dd9ce012 Webchat suppress to display of dupe messages
This patch updates the web ui for webchat to suppress the displaying
of duplicate recieved messages.  Dupes can happen over the KISS
interface due to packets being encapsulated by nearby repeaters into 3rd
party packets.

When a dupe message is recieved, the dupe message is flashed 3 times.
2023-08-22 13:37:43 -04:00
hemna 1d6a667987 Convert webchat internet urls to local static urls 2023-08-22 12:51:50 -04:00
hemna 2e9a204c74 Make use of webchat gps config options
This patch makes use of the gps settings in the webchat section.
If the user sets the latitude and longitude in the config file, then
the gps beacon button will be enabled.  The gps button will still be
enabled if the http connection is over SSL.
2023-08-22 12:31:44 -04:00
hemna f922b3f97b Added new webchat config section
This patch adds a new webchat config section to specify:
web_ip (the ip address to listen on)
web_port
latitude (latitude to use for the GPS beacon button)
longitude (long to use for the GPS beacon button)
2023-08-22 12:01:34 -04:00
hemna 8dd3b05bb1 fixed webchat logging.logformat typeoh
This fixes a problem with webchat when specifying the logfile
in aprsd config
2023-08-15 21:49:43 -04:00
hemna e06305fceb prep for 3.1.3 2023-08-15 17:52:36 -04:00
hemna 33c7871dbe Forcefully allow development webchat flask
This patch adds the force werkzeug to allow development environment
to allow aprsd webchat to work from inside of systemd
2023-08-15 17:42:56 -04:00
hemna b2f95b0f4e Updated Changelog for 3.1.2 2023-08-15 15:25:01 -04:00
hemna ae9e4d31ad Added support for ThirdParty packet types
The kiss clients now detect if the incomming packet is a third party
packet and then sends up the subpacket instead of the encapsulated
packet up to the consumer.
2023-08-15 14:24:03 -04:00
hemna 65a5a90458 Disable the Send GPS Beacon button
This patch disables the 'Send GPS Beacon' button for the webchat
command if the browser isn't connected over https
2023-08-14 18:45:13 -04:00
hemna 182887c20a Removed adhoc ssl support in webchat
This immediately breaks the beacon button.
This patch removes the dep for pyopenssl and cryptography
so that aprsd install on the rpi.

Unfortunately in order for the web page to get the Lat/Lon, the
browser must be connected over SSL.  Will have to create a workaround
for this later.
2023-08-14 18:34:25 -04:00
hemna f228144f4b Updated Changelog for v3.1.1 2023-08-07 13:01:52 -04:00
hemna db9e1d23d1 Fixed pep8 failures 2023-08-07 11:07:01 -04:00
hemna 986df391b2 re-enable USWeatherPlugin to use mapClick
The old MApClick.php api seems to work...re-enabling
2023-07-31 21:53:02 -04:00
hemna 3994235380 Merge pull request #128 from craigerl/fix_kiss
Fix sending packets over KISS interface
2023-07-28 18:08:54 -04:00
hemna 9ebf2f9a30 Fix sending packets over KISS interface
The KISS client sends the path as part of the headers, so we had
to strip out the path from the payload of each message so the path
wouldn't get listed twice.
2023-07-28 17:25:06 -04:00
hemna 011cfc55e1 Use config web_ip for running admin ui from module
When running the web admin interface with
'python -m aprsd.wsgi' the Flask app global now uses
the web_ip config entry for listening.  Also disabled
debug output.
2023-07-26 08:47:22 -04:00
hemna e0c3c5cbbf remove loop log 2023-07-25 20:45:55 -04:00
hemna 26f354b3a9 Max out the client reconnect backoff to 5
This patch adjusts the backoff mechanism for aprs client
reconnect to a max backoff sleep of 5 seconds.   This prevents
an exponential backoff when connection retrying.
2023-07-24 17:03:29 -04:00
hemna 922a6dbb35 Merge pull request #125 from craigerl/update-Dockerfile
Update the Dockerfile
2023-07-24 14:34:06 -04:00
hemna d03c4fc096 Update the Dockerfile
This updates the main Dockerfile to be the same as the
Dockerfile-dev other than using the official pypi package for
aprsd.
2023-07-24 11:36:28 -04:00
45 changed files with 2012 additions and 671 deletions
+1 -1
View File
@@ -17,7 +17,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.8", "3.9", "3.10", "3.11"]
python-version: ["3.9", "3.10", "3.11"]
steps:
- uses: actions/checkout@v2
- name: Set up Python ${{ matrix.python-version }}
+1 -1
View File
@@ -7,7 +7,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.8", "3.9", "3.10"]
python-version: ["3.9", "3.10", "3.11"]
steps:
- uses: actions/checkout@v2
- name: Set up Python ${{ matrix.python-version }}
+1 -2
View File
@@ -6,8 +6,7 @@ on:
aprsd_version:
required: true
options:
- 2.5.9
- 2.6.0
- 3.0.0
logLevel:
description: 'Log level'
required: true
-1
View File
@@ -5,7 +5,6 @@ repos:
- id: trailing-whitespace
- id: end-of-file-fixer
- id: check-yaml
- id: check-added-large-files
- id: detect-private-key
- id: check-merge-conflict
- id: check-case-conflict
+41
View File
@@ -1,9 +1,50 @@
CHANGES
=======
* Removed admin\_web.py
* Removed some RPC server log noise
* Fixed admin page packet date
* RPC Server logs the client IP on failed auth
* Start keepalive thread first
* fixed an issue in the mobile webchat
* Added dupe checkig code to webchat mobile
* click on the div after added
* Webchat suppress to display of dupe messages
* Convert webchat internet urls to local static urls
* Make use of webchat gps config options
* Added new webchat config section
* fixed webchat logging.logformat typeoh
v3.1.3
------
* prep for 3.1.3
* Forcefully allow development webchat flask
v3.1.2
------
* Updated Changelog for 3.1.2
* Added support for ThirdParty packet types
* Disable the Send GPS Beacon button
* Removed adhoc ssl support in webchat
v3.1.1
------
* Updated Changelog for v3.1.1
* Fixed pep8 failures
* re-enable USWeatherPlugin to use mapClick
* Fix sending packets over KISS interface
* Use config web\_ip for running admin ui from module
* remove loop log
* Max out the client reconnect backoff to 5
* Update the Dockerfile
v3.1.0
------
* Changelog updates for v3.1.0
* Use CONF.admin.web\_port for single launch web admin
* Fixed sio namespace registration
* Update Dockerfile-dev to include uwsgi
-360
View File
@@ -1,360 +0,0 @@
import datetime
import json
import logging
from logging.handlers import RotatingFileHandler
import time
import flask
from flask.logging import default_handler
import flask_classful
from flask_httpauth import HTTPBasicAuth
from flask_socketio import Namespace, SocketIO
from oslo_config import cfg
from werkzeug.security import check_password_hash, generate_password_hash
import aprsd
from aprsd import cli_helper, client, conf, packets, plugin, threads
from aprsd.log import rich as aprsd_logging
from aprsd.rpc import client as aprsd_rpc_client
CONF = cfg.CONF
LOG = logging.getLogger("APRSD")
auth = HTTPBasicAuth()
users = None
app = None
# HTTPBasicAuth doesn't work on a class method.
# This has to be out here. Rely on the APRSDFlask
# class to initialize the users from the config
@auth.verify_password
def verify_password(username, password):
global users
if username in users and check_password_hash(users.get(username), password):
return username
class APRSDFlask(flask_classful.FlaskView):
def set_config(self):
global users
self.users = {}
user = CONF.admin.user
self.users[user] = generate_password_hash(CONF.admin.password)
users = self.users
@auth.login_required
def index(self):
stats = self._stats()
print(stats)
LOG.debug(
"watch list? {}".format(
CONF.watch_list.callsigns,
),
)
wl = aprsd_rpc_client.RPCClient().get_watch_list()
if wl and wl.is_enabled():
watch_count = len(wl)
watch_age = wl.max_delta()
else:
watch_count = 0
watch_age = 0
sl = aprsd_rpc_client.RPCClient().get_seen_list()
if sl:
seen_count = len(sl)
else:
seen_count = 0
pm = plugin.PluginManager()
plugins = pm.get_plugins()
plugin_count = len(plugins)
if CONF.aprs_network.enabled:
transport = "aprs-is"
aprs_connection = (
"APRS-IS Server: <a href='http://status.aprs2.net' >"
"{}</a>".format(stats["stats"]["aprs-is"]["server"])
)
else:
# We might be connected to a KISS socket?
if client.KISSClient.kiss_enabled():
transport = client.KISSClient.transport()
if transport == client.TRANSPORT_TCPKISS:
aprs_connection = (
"TCPKISS://{}:{}".format(
CONF.kiss_tcp.host,
CONF.kiss_tcp.port,
)
)
elif transport == client.TRANSPORT_SERIALKISS:
aprs_connection = (
"SerialKISS://{}@{} baud".format(
CONF.kiss_serial.device,
CONF.kiss_serial.baudrate,
)
)
stats["transport"] = transport
stats["aprs_connection"] = aprs_connection
entries = conf.conf_to_dict()
return flask.render_template(
"index.html",
initial_stats=stats,
aprs_connection=aprs_connection,
callsign=CONF.callsign,
version=aprsd.__version__,
config_json=json.dumps(
entries, indent=4,
sort_keys=True, default=str,
),
watch_count=watch_count,
watch_age=watch_age,
seen_count=seen_count,
plugin_count=plugin_count,
)
@auth.login_required
def messages(self):
track = packets.PacketTrack()
msgs = []
for id in track:
LOG.info(track[id].dict())
msgs.append(track[id].dict())
return flask.render_template("messages.html", messages=json.dumps(msgs))
@auth.login_required
def packets(self):
packet_list = aprsd_rpc_client.RPCClient().get_packet_list()
if packet_list:
packets = packet_list.get()
tmp_list = []
for pkt in packets:
tmp_list.append(pkt.json)
return json.dumps(tmp_list)
else:
return json.dumps([])
@auth.login_required
def plugins(self):
pm = plugin.PluginManager()
pm.reload_plugins()
return "reloaded"
@auth.login_required
def save(self):
"""Save the existing queue to disk."""
track = packets.PacketTrack()
track.save()
return json.dumps({"messages": "saved"})
def _stats(self):
track = aprsd_rpc_client.RPCClient().get_packet_track()
now = datetime.datetime.now()
time_format = "%m-%d-%Y %H:%M:%S"
stats_dict = aprsd_rpc_client.RPCClient().get_stats_dict()
if not stats_dict:
stats_dict = {
"aprsd": {},
"aprs-is": {"server": ""},
"messages": {
"sent": 0,
"received": 0,
},
"email": {
"sent": 0,
"received": 0,
},
"seen_list": {
"sent": 0,
"received": 0,
},
}
# Convert the watch_list entries to age
wl = aprsd_rpc_client.RPCClient().get_watch_list()
new_list = {}
if wl:
for call in wl.get_all():
# call_date = datetime.datetime.strptime(
# str(wl.last_seen(call)),
# "%Y-%m-%d %H:%M:%S.%f",
# )
# We have to convert the RingBuffer to a real list
# so that json.dumps works.
# pkts = []
# for pkt in wl.get(call)["packets"].get():
# pkts.append(pkt)
new_list[call] = {
"last": wl.age(call),
# "packets": pkts
}
stats_dict["aprsd"]["watch_list"] = new_list
packet_list = aprsd_rpc_client.RPCClient().get_packet_list()
rx = tx = 0
if packet_list:
rx = packet_list.total_rx()
tx = packet_list.total_tx()
stats_dict["packets"] = {
"sent": tx,
"received": rx,
}
if track:
size_tracker = len(track)
else:
size_tracker = 0
result = {
"time": now.strftime(time_format),
"size_tracker": size_tracker,
"stats": stats_dict,
}
return result
def stats(self):
return json.dumps(self._stats())
class LogUpdateThread(threads.APRSDThread):
def __init__(self):
super().__init__("LogUpdate")
def loop(self):
global socketio
if socketio:
log_entries = aprsd_rpc_client.RPCClient().get_log_entries()
if log_entries:
for entry in log_entries:
socketio.emit(
"log_entry", entry,
namespace="/logs",
)
time.sleep(5)
return True
class LoggingNamespace(Namespace):
log_thread = None
def on_connect(self):
global socketio
socketio.emit(
"connected", {"data": "/logs Connected"},
namespace="/logs",
)
self.log_thread = LogUpdateThread()
self.log_thread.start()
def on_disconnect(self):
LOG.debug("LOG Disconnected")
if self.log_thread:
self.log_thread.stop()
def setup_logging(flask_app, loglevel, quiet):
flask_log = logging.getLogger("werkzeug")
flask_app.logger.removeHandler(default_handler)
flask_log.removeHandler(default_handler)
log_level = conf.log.LOG_LEVELS[loglevel]
flask_log.setLevel(log_level)
date_format = CONF.logging.date_format
flask_log.disabled = True
flask_app.logger.disabled = True
if CONF.logging.rich_logging:
log_format = "%(message)s"
log_formatter = logging.Formatter(fmt=log_format, datefmt=date_format)
rh = aprsd_logging.APRSDRichHandler(
show_thread=True, thread_width=15,
rich_tracebacks=True, omit_repeated_times=False,
)
rh.setFormatter(log_formatter)
flask_log.addHandler(rh)
log_file = CONF.logging.logfile
if log_file:
log_format = CONF.logging.logformat
log_formatter = logging.Formatter(fmt=log_format, datefmt=date_format)
fh = RotatingFileHandler(
log_file, maxBytes=(10248576 * 5),
backupCount=4,
)
fh.setFormatter(log_formatter)
flask_log.addHandler(fh)
def init_flask(loglevel, quiet):
global socketio
flask_app = flask.Flask(
"aprsd",
static_url_path="/static",
static_folder="web/admin/static",
template_folder="web/admin/templates",
)
setup_logging(flask_app, loglevel, quiet)
server = APRSDFlask()
server.set_config()
flask_app.route("/", methods=["GET"])(server.index)
flask_app.route("/stats", methods=["GET"])(server.stats)
flask_app.route("/messages", methods=["GET"])(server.messages)
flask_app.route("/packets", methods=["GET"])(server.packets)
flask_app.route("/save", methods=["GET"])(server.save)
flask_app.route("/plugins", methods=["GET"])(server.plugins)
socketio = SocketIO(
flask_app, logger=False, engineio_logger=False,
# async_mode="threading",
)
# import eventlet
# eventlet.monkey_patch()
gunicorn_logger = logging.getLogger("gunicorn.error")
flask_app.logger.handlers = gunicorn_logger.handlers
flask_app.logger.setLevel(gunicorn_logger.level)
socketio.on_namespace(LoggingNamespace("/logs"))
return socketio, flask_app
def create_app(config_file=None, log_level=None):
global socketio
global app
default_config_file = cli_helper.DEFAULT_CONFIG_FILE
if not config_file:
config_file = default_config_file
CONF(
[], project="aprsd", version=aprsd.__version__,
default_config_files=[config_file],
)
if not log_level:
log_level = CONF.logging.log_level
socketio, app = init_flask(log_level, False)
setup_logging(app, log_level, False)
return app
if __name__ == "aprsd.flask":
sio, app = create_app()
+18 -7
View File
@@ -24,7 +24,7 @@ TRANSPORT_SERIALKISS = "serialkiss"
factory = None
class Client:
class Client(metaclass=trace.TraceWrapperMetaclass):
"""Singleton client class that constructs the aprslib connection."""
_instance = None
@@ -49,8 +49,10 @@ class Client:
@property
def client(self):
if not self._client:
LOG.info("Creating APRS client")
self._client = self.setup_connection()
if self.filter:
LOG.info("Creating APRS client filter")
self._client.set_filter(self.filter)
return self._client
@@ -65,6 +67,9 @@ class Client:
else:
LOG.warning("Client not initialized, nothing to reset.")
# Recreate the client
LOG.info(f"Creating new client {self.client}")
@abc.abstractmethod
def setup_connection(self):
pass
@@ -84,7 +89,7 @@ class Client:
pass
class APRSISClient(Client):
class APRSISClient(Client, metaclass=trace.TraceWrapperMetaclass):
_client = None
@@ -133,7 +138,6 @@ class APRSISClient(Client):
"""APRS lib already decodes this."""
return core.Packet.factory(args[0])
@trace.trace
def setup_connection(self):
user = CONF.aprs_network.login
password = CONF.aprs_network.password
@@ -159,14 +163,18 @@ class APRSISClient(Client):
LOG.error(f"Unable to connect to APRS-IS server. '{e}' ")
connected = False
time.sleep(backoff)
backoff = backoff * 2
# Don't allow the backoff to go to inifinity.
if backoff > 5:
backoff = 5
else:
backoff += 1
continue
LOG.debug(f"Logging in to APRS-IS with user '{user}'")
self._client = aprs_client
return aprs_client
class KISSClient(Client):
class KISSClient(Client, metaclass=trace.TraceWrapperMetaclass):
_client = None
@@ -229,9 +237,12 @@ class KISSClient(Client):
# LOG.debug(f"Decoding {msg}")
raw = aprslib.parse(str(frame))
return core.Packet.factory(raw)
packet = core.Packet.factory(raw)
if isinstance(packet, core.ThirdParty):
return packet.subpacket
else:
return packet
@trace.trace
def setup_connection(self):
self._client = kiss.KISS3Client()
return self._client
+9 -15
View File
@@ -81,27 +81,18 @@ class KISS3Client:
LOG.debug("Start blocking KISS consumer")
self._parse_callback = callback
self.kiss.read(callback=self.parse_frame, min_frames=None)
LOG.debug("END blocking KISS consumer")
LOG.debug(f"END blocking KISS consumer {self.kiss}")
def send(self, packet):
"""Send an APRS Message object."""
# payload = (':%-9s:%s' % (
# msg.tocall,
# payload
# )).encode('US-ASCII'),
# payload = str(msg).encode('US-ASCII')
payload = None
path = ["WIDE1-1", "WIDE2-1"]
if isinstance(packet, core.AckPacket):
msg_payload = f"ack{packet.msgNo}"
elif isinstance(packet, core.Packet):
payload = packet.raw.encode("US-ASCII")
path = ["WIDE2-1"]
if isinstance(packet, core.Packet):
packet.prepare()
payload = packet.payload.encode("US-ASCII")
else:
msg_payload = f"{packet.raw}{{{str(packet.msgNo)}"
if not payload:
payload = (
":{:<9}:{}".format(
packet.to_call,
@@ -109,9 +100,12 @@ class KISS3Client:
)
).encode("US-ASCII")
LOG.debug(f"Send '{payload}' TO KISS")
LOG.debug(
f"KISS Send '{payload}' TO '{packet.to_call}' From "
f"'{packet.from_call}' with PATH '{path}'",
)
frame = Frame.ui(
destination=packet.to_call,
destination="APZ100",
source=packet.from_call,
path=path,
info=payload,
+3 -3
View File
@@ -94,6 +94,9 @@ def server(ctx, flush):
packets.WatchList().load()
packets.SeenList().load()
keepalive = threads.KeepAliveThread()
keepalive.start()
rx_thread = rx.APRSDPluginRXThread(
packet_queue=threads.packet_queue,
)
@@ -105,9 +108,6 @@ def server(ctx, flush):
packets.PacketTrack().restart()
keepalive = threads.KeepAliveThread()
keepalive.start()
if CONF.rpc_settings.enabled:
rpc = rpc_server.APRSDRPCThread()
rpc.start()
+30 -11
View File
@@ -163,7 +163,7 @@ class WebChatProcessPacketThread(rx.APRSDProcessPacketThread):
message = packet.get("message_text", None)
msg = {
"id": 0,
"id": packet.msgNo,
"ts": packet.get("timestamp", time.time()),
"ack": False,
"from": fromcall,
@@ -239,6 +239,13 @@ def index():
stats["transport"] = transport
stats["aprs_connection"] = aprs_connection
LOG.debug(f"initial stats = {stats}")
latitude = CONF.webchat.latitude
if latitude:
latitude = float(CONF.webchat.latitude)
longitude = CONF.webchat.longitude
if longitude:
longitude = float(longitude)
return flask.render_template(
html_template,
@@ -246,6 +253,8 @@ def index():
aprs_connection=aprs_connection,
callsign=CONF.callsign,
version=aprsd.__version__,
latitude=latitude,
longitude=longitude,
)
@@ -265,8 +274,12 @@ def _stats():
time_format = "%m-%d-%Y %H:%M:%S"
stats_dict = stats_obj.stats()
# Webchat doesnt need these
del stats_dict["aprsd"]["watch_list"]
del stats_dict["aprsd"]["seen_list"]
if "watch_list" in stats_dict["aprsd"]:
del stats_dict["aprsd"]["watch_list"]
if "seen_list" in stats_dict["aprsd"]:
del stats_dict["aprsd"]["seen_list"]
if "threads" in stats_dict["aprsd"]:
del stats_dict["aprsd"]["threads"]
# del stats_dict["email"]
# del stats_dict["plugins"]
# del stats_dict["messages"]
@@ -373,7 +386,7 @@ def setup_logging(flask_app, loglevel, quiet):
log_file = CONF.logging.logfile
if log_file:
log_format = CONF.loging.logformat
log_format = CONF.logging.logformat
log_formatter = logging.Formatter(fmt=log_format, datefmt=date_format)
fh = RotatingFileHandler(
log_file, maxBytes=(10248576 * 5),
@@ -423,8 +436,8 @@ def init_flask(loglevel, quiet):
"--port",
"port",
show_default=True,
default=80,
help="Port to listen to web requests",
default=None,
help="Port to listen to web requests. This overrides the config.webchat.web_port setting.",
)
@click.pass_context
@cli_helper.process_standard_options
@@ -446,6 +459,8 @@ def webchat(ctx, flush, port):
CONF.log_opt_values(LOG, logging.DEBUG)
user = CONF.admin.user
users[user] = generate_password_hash(CONF.admin.password)
if not port:
port = CONF.webchat.web_port
# Initialize the client factory and create
# The correct client object ready for use
@@ -464,6 +479,10 @@ def webchat(ctx, flush, port):
packets.WatchList()
packets.SeenList()
keepalive = threads.KeepAliveThread()
LOG.info("Start KeepAliveThread")
keepalive.start()
socketio = init_flask(loglevel, quiet)
rx_thread = rx.APRSDPluginRXThread(
packet_queue=threads.packet_queue,
@@ -475,15 +494,15 @@ def webchat(ctx, flush, port):
)
process_thread.start()
keepalive = threads.KeepAliveThread()
LOG.info("Start KeepAliveThread")
keepalive.start()
LOG.info("Start socketio.run()")
socketio.run(
flask_app,
ssl_context="adhoc",
host=CONF.admin.web_ip,
# This is broken for now after removing cryptography
# and pyopenssl
# ssl_context="adhoc",
host=CONF.webchat.web_ip,
port=port,
allow_unsafe_werkzeug=True,
)
LOG.info("WebChat exiting!!!! Bye.")
+30
View File
@@ -19,6 +19,10 @@ rpc_group = cfg.OptGroup(
name="rpc_settings",
title="RPC Settings for admin <--> web",
)
webchat_group = cfg.OptGroup(
name="webchat",
title="Settings specific to the webchat command",
)
aprsd_opts = [
@@ -163,6 +167,29 @@ enabled_plugins_opts = [
),
]
webchat_opts = [
cfg.IPOpt(
"web_ip",
default="0.0.0.0",
help="The ip address to listen on",
),
cfg.PortOpt(
"web_port",
default=8001,
help="The port to listen on",
),
cfg.StrOpt(
"latitude",
default=None,
help="Latitude for the GPS Beacon button. If not set, the button will not be enabled.",
),
cfg.StrOpt(
"longitude",
default=None,
help="Longitude for the GPS Beacon button. If not set, the button will not be enabled.",
),
]
def register_opts(config):
config.register_opts(aprsd_opts)
@@ -173,6 +200,8 @@ def register_opts(config):
config.register_opts(watch_list_opts, group=watch_list_group)
config.register_group(rpc_group)
config.register_opts(rpc_opts, group=rpc_group)
config.register_group(webchat_group)
config.register_opts(webchat_opts, group=webchat_group)
def list_opts():
@@ -181,4 +210,5 @@ def list_opts():
admin_group.name: admin_opts,
watch_list_group.name: watch_list_opts,
rpc_group.name: rpc_opts,
webchat_group.name: webchat_opts,
}
+95 -39
View File
@@ -25,6 +25,7 @@ PACKET_TYPE_OBJECT = "object"
PACKET_TYPE_UNKNOWN = "unknown"
PACKET_TYPE_STATUS = "status"
PACKET_TYPE_BEACON = "beacon"
PACKET_TYPE_THIRDPARTY = "thirdparty"
PACKET_TYPE_UNCOMPRESSED = "uncompressed"
@@ -57,6 +58,8 @@ class Packet(metaclass=abc.ABCMeta):
# or holds the raw string from input packet
raw: str = None
raw_dict: dict = field(repr=False, default_factory=lambda: {})
# Built by calling prepare(). raw needs this built first.
payload: str = None
# Fields related to sending packets out
send_count: int = field(repr=False, default=0)
@@ -92,14 +95,28 @@ class Packet(metaclass=abc.ABCMeta):
def prepare(self):
"""Do stuff here that is needed prior to sending over the air."""
# now build the raw message for sending
self._build_payload()
self._build_raw()
def _build_payload(self):
"""The payload is the non headers portion of the packet."""
msg = self._filter_for_send().rstrip("\n")
self.payload = (
f":{self.to_call.ljust(9)}"
f":{msg}"
)
def _build_raw(self):
"""Build the self.raw string which is what is sent over the air."""
self.raw = self._filter_for_send().rstrip("\n")
"""Build the self.raw which is what is sent over the air."""
self.raw = "{}>APZ100:{}".format(
self.from_call,
self.payload,
)
LOG.debug(f"_build_raw: payload '{self.payload}' raw '{self.raw}'")
@staticmethod
def factory(raw_packet):
"""Factory method to create a packet from a raw packet string."""
raw = raw_packet
raw["raw_dict"] = raw.copy()
translate_fields = {
@@ -118,6 +135,19 @@ class Packet(metaclass=abc.ABCMeta):
packet_type = get_packet_type(raw)
raw["packet_type"] = packet_type
class_name = TYPE_LOOKUP[packet_type]
if packet_type == PACKET_TYPE_THIRDPARTY:
# We have an encapsulated packet!
# So we need to decode it and return the inner packet
# as the packet we are going to process.
# This is a recursive call to the factory
subpacket_raw = raw["subpacket"]
subpacket = Packet.factory(subpacket_raw)
del raw["subpacket"]
# raw["subpacket"] = subpacket
packet = dacite.from_dict(data_class=class_name, data=raw)
packet.subpacket = subpacket
return packet
if packet_type == PACKET_TYPE_UNKNOWN:
# Try and figure it out here
if "latitude" in raw:
@@ -233,7 +263,7 @@ class PathPacket(Packet):
path: List[str] = field(default_factory=list)
via: str = None
def _build_raw(self):
def _build_payload(self):
raise NotImplementedError
@@ -245,13 +275,8 @@ class AckPacket(PathPacket):
if self.response:
LOG.warning("Response set!")
def _build_raw(self):
"""Build the self.raw which is what is sent over the air."""
self.raw = "{}>APZ100::{}:ack{}".format(
self.from_call,
self.to_call.ljust(9),
self.msgNo,
)
def _build_payload(self):
self.payload = f":{self.to_call.ljust(9)}:ack{self.msgNo}"
@dataclass
@@ -262,13 +287,8 @@ class RejectPacket(PathPacket):
if self.response:
LOG.warning("Response set!")
def _build_raw(self):
"""Build the self.raw which is what is sent over the air."""
self.raw = "{}>APZ100::{} :rej{}".format(
self.from_call,
self.to_call.ljust(9),
self.msgNo,
)
def _build_payload(self):
self.payload = f":{self.to_call.ljust(9)} :rej{self.msgNo}"
@dataclass
@@ -285,10 +305,8 @@ class MessagePacket(PathPacket):
# We all miss George Carlin
return re.sub("fuck|shit|cunt|piss|cock|bitch", "****", message)
def _build_raw(self):
"""Build the self.raw which is what is sent over the air."""
self.raw = "{}>APZ100::{}:{}{{{}".format(
self.from_call,
def _build_payload(self):
self.payload = ":{}:{}{{{}".format(
self.to_call.ljust(9),
self._filter_for_send().rstrip("\n"),
str(self.msgNo),
@@ -301,7 +319,7 @@ class StatusPacket(PathPacket):
messagecapable: bool = False
comment: str = None
def _build_raw(self):
def _build_payload(self):
raise NotImplementedError
@@ -400,16 +418,24 @@ class GPSPacket(PathPacket):
time_zulu = result_utc_datetime.strftime("%d%H%M")
return time_zulu
def _build_raw(self):
def _build_payload(self):
"""The payload is the non headers portion of the packet."""
time_zulu = self._build_time_zulu()
lat = self.latitude
long = self.longitude
self.payload = (
f"@{time_zulu}z{lat}{self.symbol_table}"
f"{long}{self.symbol}"
)
if self.comment:
self.payload = f"{self.payload}{self.comment}"
def _build_raw(self):
self.raw = (
f"{self.from_call}>{self.to_call},WIDE2-1:"
f"@{time_zulu}z{self.latitude}{self.symbol_table}"
f"{self.longitude}{self.symbol}"
f"{self.payload}"
)
if self.comment:
self.raw = f"{self.raw}{self.comment}"
@dataclass
@@ -422,7 +448,7 @@ class MicEPacket(GPSPacket):
# 0 to 360
course: int = 0
def _build_raw(self):
def _build_payload(self):
raise NotImplementedError
@@ -436,6 +462,19 @@ class ObjectPacket(GPSPacket):
# 0 to 360
course: int = 0
def _build_payload(self):
time_zulu = self._build_time_zulu()
lat = self.convert_latitude(self.latitude)
long = self.convert_longitude(self.longitude)
self.payload = (
f"*{time_zulu}z{lat}{self.symbol_table}"
f"{long}{self.symbol}"
)
if self.comment:
self.payload = f"{self.payload}{self.comment}"
def _build_raw(self):
"""
REPEAT builds packets like
@@ -446,17 +485,11 @@ class ObjectPacket(GPSPacket):
callsign is the station callsign for the object
The frequency, uplink_tone, offset is part of the comment
"""
time_zulu = self._build_time_zulu()
lat = self.convert_latitude(self.latitude)
long = self.convert_longitude(self.longitude)
self.raw = (
f"{self.from_call}>APZ100:;{self.to_call:9s}"
f"*{time_zulu}z{lat}{self.symbol_table}"
f"{long}{self.symbol}"
f"{self.payload}"
)
if self.comment:
self.raw = f"{self.raw}{self.comment}"
@dataclass()
@@ -474,7 +507,7 @@ class WeatherPacket(GPSPacket):
pressure: float = 0.00
comment: str = None
def _build_raw(self):
def _build_payload(self):
"""Build an uncompressed weather packet
Format =
@@ -502,7 +535,6 @@ class WeatherPacket(GPSPacket):
time_zulu = self._build_time_zulu()
contents = [
f"{self.from_call}>{self.to_call},WIDE1-1,WIDE2-1:",
f"@{time_zulu}z{self.latitude}{self.symbol_table}",
f"{self.longitude}{self.symbol}",
f"{self.wind_direction:03d}",
@@ -523,11 +555,32 @@ class WeatherPacket(GPSPacket):
# Barometric pressure (in tenths of millibars/tenths of hPascal)
f"b{self.pressure:05.0f}",
]
if self.comment:
contents.append(self.comment)
self.payload = "".join(contents)
self.raw = "".join(contents)
def _build_raw(self):
self.raw = (
f"{self.from_call}>{self.to_call},WIDE1-1,WIDE2-1:"
f"{self.payload}"
)
class ThirdParty(Packet):
# Holds the encapsulated packet
subpacket: Packet = None
def __repr__(self):
"""Build the repr version of the packet."""
repr_str = (
f"{self.__class__.__name__}:"
f" From: {self.from_call} "
f" To: {self.to_call} "
f" Subpacket: {repr(self.subpacket)}"
)
return repr_str
TYPE_LOOKUP = {
@@ -540,6 +593,7 @@ TYPE_LOOKUP = {
PACKET_TYPE_STATUS: StatusPacket,
PACKET_TYPE_BEACON: GPSPacket,
PACKET_TYPE_UNKNOWN: Packet,
PACKET_TYPE_THIRDPARTY: ThirdParty,
}
@@ -566,6 +620,8 @@ def get_packet_type(packet: dict):
elif pkt_format == PACKET_TYPE_UNCOMPRESSED:
if packet.get("symbol", None) == "_":
packet_type = PACKET_TYPE_WX
elif pkt_format == PACKET_TYPE_THIRDPARTY:
packet_type = PACKET_TYPE_THIRDPARTY
return packet_type
+3 -3
View File
@@ -33,9 +33,9 @@ def get_weather_gov_for_gps(lat, lon):
)
try:
url2 = (
# "https://forecast.weather.gov/MapClick.php?lat=%s"
# "&lon=%s&FcstType=json" % (lat, lon)
f"https://api.weather.gov/points/{lat},{lon}"
"https://forecast.weather.gov/MapClick.php?lat=%s"
"&lon=%s&FcstType=json" % (lat, lon)
# f"https://api.weather.gov/points/{lat},{lon}"
)
LOG.debug(f"Fetching weather '{url2}'")
response = requests.get(url2, headers=headers)
+8 -1
View File
@@ -37,11 +37,18 @@ class USWeatherPlugin(plugin.APRSDRegexCommandPluginBase, plugin.APRSFIKEYMixin)
def process(self, packet):
LOG.info("Weather Plugin")
fromcall = packet.from_call
message = packet.get("message_text", None)
# 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)
searchcall = searchcall.upper()
else:
searchcall = fromcall
api_key = CONF.aprs_fi.apiKey
try:
aprs_data = plugin_utils.get_aprs_fi(api_key, fromcall)
aprs_data = plugin_utils.get_aprs_fi(api_key, searchcall)
except Exception as ex:
LOG.error(f"Failed to fetch aprs.fi data {ex}")
return "Failed to fetch aprs.fi location"
+11 -11
View File
@@ -16,9 +16,17 @@ LOG = logging.getLogger("APRSD")
def magic_word_authenticator(sock):
client_ip = sock.getpeername()[0]
magic = sock.recv(len(CONF.rpc_settings.magic_word)).decode()
if magic != CONF.rpc_settings.magic_word:
raise AuthenticationError(f"wrong magic word {magic}")
LOG.error(
f"wrong magic word passed from {client_ip} "
"'{magic}' != '{CONF.rpc_settings.magic_word}'",
)
raise AuthenticationError(
f"wrong magic word passed in '{magic}'"
f" != '{CONF.rpc_settings.magic_word}'",
)
return sock, None
@@ -49,51 +57,43 @@ class APRSDService(rpyc.Service):
def on_connect(self, conn):
# code that runs when a connection is created
# (to init the service, if needed)
LOG.info("Connected")
LOG.info("RPC Client Connected")
self._conn = conn
def on_disconnect(self, conn):
# code that runs after the connection has already closed
# (to finalize the service, if needed)
LOG.info("Disconnected")
LOG.info("RPC Client Disconnected")
self._conn = None
@rpyc.exposed
def get_stats(self):
LOG.info("get_stats")
stat = stats.APRSDStats()
stats_dict = stat.stats()
return_str = json.dumps(stats_dict, indent=4, sort_keys=True, default=str)
LOG.info(f"get_stats: return size {len(return_str)}")
return return_str
@rpyc.exposed
def get_stats_obj(self):
LOG.info("get_stats_obj")
return stats.APRSDStats()
@rpyc.exposed
def get_packet_list(self):
LOG.info("get_packet_list")
return packets.PacketList()
@rpyc.exposed
def get_packet_track(self):
LOG.info("get_packet_track")
return packets.PacketTrack()
@rpyc.exposed
def get_watch_list(self):
LOG.info("get_watch_list")
return packets.WatchList()
@rpyc.exposed
def get_seen_list(self):
LOG.info("get_seen_list")
return packets.SeenList()
@rpyc.exposed
def get_log_entries(self):
LOG.info("get_log_entries")
entries = log_monitor.LogEntries().get_all_and_purge()
return json.dumps(entries, default=str)
-2
View File
@@ -246,8 +246,6 @@ class APRSDStats:
},
"plugins": plugin_stats,
}
LOG.debug(f"STATS = {stats}")
LOG.info("APRSD Stats: DONE")
return stats
def __str__(self):
+5 -5
View File
@@ -111,14 +111,14 @@ function update_packets( data ) {
pkt = JSON.parse(val);
update_watchlist_from_packet(pkt['from_call'], pkt);
if ( packet_list.hasOwnProperty(pkt.timestamp) == false ) {
if ( packet_list.hasOwnProperty(pkt['timestamp']) == false ) {
// Store the packet
packet_list[pkt.timestamp] = pkt;
packet_list[pkt['timestamp']] = pkt;
//ts_str = val["timestamp"].toString();
//ts = ts_str.split(".")[0]*1000;
ts = pkt.timestamp
var d = new Date(ts).toLocaleDateString("en-US");
var t = new Date(ts).toLocaleTimeString("en-US");
ts = pkt['timestamp'] * 1000;
var d = new Date(ts).toLocaleDateString();
var t = new Date(ts).toLocaleTimeString();
var from_call = pkt.from_call;
if (from_call == our_callsign) {
title_id = 'title_tx';
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
+9 -1
View File
@@ -1,9 +1,17 @@
function init_gps() {
console.log("init_gps Called.")
console.log("latitude: "+latitude)
console.log("longitude: "+longitude)
$("#send_beacon").click(function() {
console.log("Send a beacon!")
getLocation();
if (!isNaN(latitude) && !isNaN(longitude)) {
// webchat admin has hard coded lat/long in the config file
showPosition({'coords': {'latitude': latitude, 'longitude': longitude}})
} else {
// Try to get the current location from the browser
getLocation();
}
});
}
@@ -1,11 +1,12 @@
var cleared = false;
var callsign_list = {};
var message_list = {};
var from_msg_list = {};
const socket = io("/sendmsg");
function size_dict(d){c=0; for (i in d) ++c; return c}
function init_chat() {
const socket = io("/sendmsg");
socket.on('connect', function () {
console.log("Connected to socketio");
});
@@ -43,7 +44,6 @@ function init_chat() {
}
socket.emit("send", msg);
$('#message').val('');
$('#to_call').val('');
});
init_gps();
@@ -118,8 +118,9 @@ function append_message_html(callsign, msg_html, new_callsign) {
$(divname).animate({scrollTop: $(divname)[0].scrollHeight}, "slow");
}
function create_message_html(time, from, to, message, ack) {
msg_html = '<div class="item">';
function create_message_html(time, from, to, message, ack, msg) {
div_id = from + "_" + msg.id;
msg_html = '<div class="item" id="'+div_id+'">';
msg_html += '<div class="tiny text">'+time+'</div>';
msg_html += '<div class="middle aligned content">';
msg_html += '<div class="tiny red header">'+from+'</div>';
@@ -136,6 +137,13 @@ function create_message_html(time, from, to, message, ack) {
return msg_html
}
function flash_message(msg) {
// Callback function to bring a hidden box back
id = msg.from + "_" + msg.id;
var msgid = $('#'+id);
msgid.effect("pulsate", { times:3 }, 2000);
}
function sent_msg(msg) {
var msgsdiv = $("#sendMsgsDiv");
@@ -147,12 +155,27 @@ function sent_msg(msg) {
var d = new Date(ts).toLocaleDateString("en-US")
var t = new Date(ts).toLocaleTimeString("en-US")
msg_html = create_message_html(t, msg['from'], msg['to'], msg['message'], ack_id);
msg_html = create_message_html(t, msg['from'], msg['to'], msg['message'], ack_id, msg);
append_message(msg['to'], msg, msg_html);
}
function from_msg(msg) {
var msgsdiv = $("#sendMsgsDiv");
console.log(msg);
if (!from_msg_list.hasOwnProperty(msg.from)) {
from_msg_list[msg.from] = new Array();
}
if (msg.id in from_msg_list[msg.from]) {
// We already have this message
console.log("We already have this message " + msg);
// Do some flashy thing?
flash_message(msg);
return false
} else {
console.log("Adding message " + msg.id + " to " + msg.from);
from_msg_list[msg.from][msg.id] = msg
}
// We have an existing entry
ts_str = msg["ts"].toString();
@@ -164,7 +187,7 @@ function from_msg(msg) {
var t = new Date(ts).toLocaleTimeString("en-US")
from = msg['from']
msg_html = create_message_html(t, from, false, msg['message'], false);
msg_html = create_message_html(t, from, false, msg['message'], false, msg);
append_message(from, msg, msg_html);
}
+208 -56
View File
@@ -1,8 +1,13 @@
var cleared = false;
var callsign_list = {};
var message_list = {};
var from_msg_list = {};
const socket = io("/sendmsg");
MSG_TYPE_TX = "tx";
MSG_TYPE_RX = "rx";
MSG_TYPE_ACK = "ack";
function size_dict(d){c=0; for (i in d) ++c; return c}
function init_chat() {
@@ -15,24 +20,28 @@ function init_chat() {
});
socket.on("sent", function(msg) {
if (cleared == false) {
if (cleared === false) {
console.log("CLEARING #msgsTabsDiv");
var msgsdiv = $("#msgsTabsDiv");
msgsdiv.html('')
cleared = true
msgsdiv.html('');
cleared = true;
}
msg["type"] = MSG_TYPE_TX;
sent_msg(msg);
});
socket.on("ack", function(msg) {
update_msg(msg);
msg["type"] = MSG_TYPE_ACK;
ack_msg(msg);
});
socket.on("new", function(msg) {
if (cleared == false) {
if (cleared === false) {
var msgsdiv = $("#msgsTabsDiv");
msgsdiv.html('')
cleared = true
cleared = true;
}
msg["type"] = MSG_TYPE_RX;
from_msg(msg);
});
@@ -46,32 +55,124 @@ function init_chat() {
});
init_gps();
// Try and load any existing chat threads from last time
init_messages();
}
function add_callsign(callsign) {
/* Ensure a callsign exists in the left hand nav */
function message_ts_id(msg) {
//Create a 'id' from the message timestamp
ts_str = msg["ts"].toString();
ts = ts_str.split(".")[0]*1000;
id = ts_str.split('.')[0];
return {'timestamp': ts, 'id': id};
}
if (callsign in callsign_list) {
return false
}
function time_ack_from_msg(msg) {
// Return the time and ack_id from a message
ts_id = message_ts_id(msg);
ts = ts_id['timestamp'];
id = ts_id['id'];
ack_id = "ack_" + id
var d = new Date(ts).toLocaleDateString("en-US")
var t = new Date(ts).toLocaleTimeString("en-US")
return {'time': t, 'date': d, 'ack_id': ack_id};
}
function save_data() {
// Save the relevant data to local storage
localStorage.setItem('callsign_list', JSON.stringify(callsign_list));
localStorage.setItem('message_list', JSON.stringify(message_list));
}
function init_messages() {
// This tries to load any previous conversations from local storage
callsign_list = JSON.parse(localStorage.getItem('callsign_list'));
message_list = JSON.parse(localStorage.getItem('message_list'));
console.log("init_messages");
if (callsign_list == null) {
callsign_list = {};
}
if (message_list == null) {
message_list = {};
}
console.log(callsign_list);
console.log(message_list);
// Now loop through each callsign and add the tabs
first_callsign = null;
for (callsign in callsign_list) {
console.log("Adding callsign " + callsign);
if (first_callsign === null) {
first_callsign = callsign;
console.log("first_callsign " + first_callsign)
}
create_callsign_tab(callsign);
}
// and then populate the messages in order
for (callsign in message_list) {
new_callsign = true;
cleared = true;
for (id in message_list[callsign]) {
msg = message_list[callsign][id];
info = time_ack_from_msg(msg);
t = info['time'];
ack_id = false;
acked = false;
if (msg['type'] == MSG_TYPE_TX) {
ack_id = info['ack_id'];
acked = msg['ack'];
}
msg_html = create_message_html(t, msg['from'], msg['to'], msg['message'], ack_id, msg, acked);
append_message_html(callsign, msg_html, new_callsign);
new_callsign = false;
}
}
//Click on the very first tab
if (first_callsign !== null) {
click_div = '#'+tab_string(first_callsign);
var click_timer = setTimeout(function() {
console.log("Click on first tab " + click_div);
$(click_div).click();
clearTimeout(click_timer);
}, 500);
}
}
function create_callsign_tab(callsign) {
//Create the html for the callsign tab and insert it into the DOM
console.log("create_callsign_tab " + callsign)
var callsignTabs = $("#callsignTabs");
tab_name = tab_string(callsign);
tab_content = tab_content_name(callsign);
divname = content_divname(callsign);
item_html = '<div class="tablinks" id="'+tab_name+'" onclick="openCallsign(event, \''+callsign+'\');">'+callsign+'</div>';
console.log(item_html);
callsignTabs.append(item_html);
}
function add_callsign(callsign) {
/* Ensure a callsign exists in the left hand nav */
if (callsign in callsign_list) {
return false
}
create_callsign_tab(callsign);
callsign_list[callsign] = true;
return true
}
function append_message(callsign, msg, msg_html) {
console.log("append_message " + callsign + " " + msg + " " + msg_html);
new_callsign = false
if (!message_list.hasOwnProperty(callsign)) {
message_list[callsign] = new Array();
//message_list[callsign] = new Array();
message_list[callsign] = {};
}
message_list[callsign].push(msg);
ts_id = message_ts_id(msg);
id = ts_id['id']
message_list[callsign][id] = msg;
// Find the right div to place the html
new_callsign = add_callsign(callsign);
@@ -97,27 +198,43 @@ function content_divname(callsign) {
function append_message_html(callsign, msg_html, new_callsign) {
var msgsTabs = $('#msgsTabsDiv');
console.log("append_message_html " + callsign + " " + msg_html + " " + new_callsign);
divname_str = tab_content_name(callsign);
divname = content_divname(callsign);
if (new_callsign) {
// we have to add a new DIV
console.log("new callsign, create msg_div_html ");
msg_div_html = '<div class="tabcontent" id="'+divname_str+'" style="height:450px;">'+msg_html+'</div>';
console.log(msg_div_html);
msgsTabs.append(msg_div_html);
} else {
var msgDiv = $(divname);
console.log("Appending ("+ msg_html + ") to " + divname);
console.log(msgDiv);
msgDiv.append(msg_html);
console.log(msgDiv);
}
console.log("divname " + divname);
console.log($(divname).length);
$(divname).animate({scrollTop: $(divname)[0].scrollHeight}, "slow");
if ($(divname).length > 0) {
$(divname).animate({scrollTop: $(divname)[0].scrollHeight}, "slow");
}
$(divname).trigger('click');
}
function create_message_html(time, from, to, message, ack) {
msg_html = '<div class="item">';
function create_message_html(time, from, to, message, ack_id, msg, acked=false) {
div_id = from + "_" + msg.id;
msg_html = '<div class="item" id="'+div_id+'">';
msg_html += '<div class="tiny text">'+time+'</div>';
msg_html += '<div class="middle aligned content">';
msg_html += '<div class="tiny red header">'+from+'</div>';
if (ack) {
msg_html += '<i class="thumbs down outline icon" id="' + ack_id + '" data-content="Waiting for ACK"></i>';
if (ack_id) {
if (acked) {
msg_html += '<div class="middle aligned content"><i class="thumbs up outline icon" id="' + ack_id + '" data-content="Message ACKed"></i></div>';
} else {
msg_html += '<div class="middle aligned content"><i class="thumbs down outline icon" id="' + ack_id + '" data-content="Waiting for ACK"></i></div>';
}
} else {
msg_html += '<i class="phone volume icon" data-content="Recieved Message"></i>';
}
@@ -129,60 +246,90 @@ function create_message_html(time, from, to, message, ack) {
return msg_html
}
function flash_message(msg) {
// Callback function to bring a hidden box back
id = msg.from + "_" + msg.id;
var msgid = $('#'+id);
msgid.effect("pulsate", { times:3 }, 2000);
}
function sent_msg(msg) {
var msgsdiv = $("#sendMsgsDiv");
info = time_ack_from_msg(msg);
t = info['time'];
ack_id = info['ack_id'];
ts_str = msg["ts"].toString();
ts = ts_str.split(".")[0]*1000;
id = ts_str.split('.')[0]
ack_id = "ack_" + id
var d = new Date(ts).toLocaleDateString("en-US")
var t = new Date(ts).toLocaleTimeString("en-US")
msg_html = create_message_html(t, msg['from'], msg['to'], msg['message'], ack_id);
msg_html = create_message_html(t, msg['from'], msg['to'], msg['message'], ack_id, msg, false);
append_message(msg['to'], msg, msg_html);
save_data();
}
function from_msg(msg) {
var msgsdiv = $("#sendMsgsDiv");
console.log(msg);
if (!from_msg_list.hasOwnProperty(msg.from)) {
from_msg_list[msg.from] = new Array();
}
// We have an existing entry
ts_str = msg["ts"].toString();
ts = ts_str.split(".")[0]*1000;
id = ts_str.split('.')[0]
ack_id = "ack_" + id
if (msg.id in from_msg_list[msg.from]) {
// We already have this message
console.log("We already have this message " + msg);
// Do some flashy thing?
flash_message(msg);
return false
} else {
console.log("Adding message " + msg.id + " to " + msg.from);
from_msg_list[msg.from][msg.id] = msg
}
var d = new Date(ts).toLocaleDateString("en-US")
var t = new Date(ts).toLocaleTimeString("en-US")
info = time_ack_from_msg(msg);
t = info['time'];
ack_id = info['ack_id'];
from = msg['from']
msg_html = create_message_html(t, from, false, msg['message'], false);
msg_html = create_message_html(t, from, false, msg['message'], false, msg, false);
append_message(from, msg, msg_html);
save_data();
}
function update_msg(msg) {
var msgsdiv = $("#sendMsgsDiv");
// We have an existing entry
ts_str = msg["ts"].toString();
id = ts_str.split('.')[0]
pretty_id = "pretty_" + id
loader_id = "loader_" + id
ack_id = "ack_" + id
span_id = "span_" + id
function ack_msg(msg) {
// Acknowledge a message
console.log("ack_msg ");
console.log(msg);
console.log(message_list);
// We have an existing entry
ts_id = message_ts_id(msg);
console.log(ts_id)
id = ts_id['id'];
//Mark the message as acked
callsign = msg['to'];
// Ensure the message_list has this callsign
if (!message_list.hasOwnProperty(callsign)) {
console.log("No message_list for " + callsign);
return false
}
// Ensure the message_list has this id
if (!message_list[callsign].hasOwnProperty(id)) {
console.log("No message_list for " + callsign + " " + id);
return false
}
console.log("Marking message as acked " + callsign + " " + id)
if (message_list[callsign][id]['ack'] == true) {
console.log("Message already acked");
return false;
}
message_list[callsign][id]['ack'] = true;
ack_id = "ack_" + id
if (msg['ack'] == true) {
var ack_div = $('#' + ack_id);
//ack_div.removeClass('thumbs up outline icon');
ack_div.removeClass('thumbs down outline icon');
ack_div.addClass('thumbs up outline icon');
}
if (msg['ack'] == true) {
var loader_div = $('#' + loader_id);
var ack_div = $('#' + ack_id);
loader_div.removeClass('ui active inline loader');
loader_div.addClass('ui disabled loader');
ack_div.removeClass('thumbs up outline icon');
ack_div.addClass('thumbs up outline icon');
}
$('.ui.accordion').accordion('refresh');
$('.ui.accordion').accordion('refresh');
save_data();
}
function callsign_select(callsign) {
@@ -197,16 +344,21 @@ function reset_Tabs() {
}
}
function openCallsign(evt, callsign) {
// This is called when a callsign tab is clicked
var i, tabcontent, tablinks;
tab_content = tab_content_name(callsign);
tabcontent = document.getElementsByClassName("tabcontent");
console.log(tabcontent);
for (i = 0; i < tabcontent.length; i++) {
tabcontent[i].style.display = "none";
}
tablinks = document.getElementsByClassName("tablinks");
console.log(tablinks);
for (i = 0; i < tablinks.length; i++) {
tablinks[i].className = tablinks[i].className.replace(" active", "");
}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+28 -7
View File
@@ -1,13 +1,19 @@
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1">
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
<link rel="stylesheet" href="https://ajax.googleapis.com/ajax/libs/jqueryui/1.12.1/themes/smoothness/jquery-ui.css">
<script src="https://ajax.googleapis.com/ajax/libs/jqueryui/1.12.1/jquery-ui.min.js"></script>
<script src="https://cdn.socket.io/4.1.2/socket.io.min.js" integrity="sha384-toS6mmwu70G0fw54EGlWWeA4z3dyJ+dlXBtSURSKN4vyRFOcxd3Bzjj/AoOwY+Rg" crossorigin="anonymous"></script>
<!-- <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script> -->
<script src="/static/js/upstream/jquery.min.js"></script>
<!-- <link rel="stylesheet" href="https://ajax.googleapis.com/ajax/libs/jqueryui/1.12.1/themes/smoothness/jquery-ui.css"> -->
<link rel="stylesheet" href="/static/css/upstream/jquery-ui.css">
<!-- <script src="https://ajax.googleapis.com/ajax/libs/jqueryui/1.12.1/jquery-ui.min.js"></script> -->
<script src="/static/js/upstream/jquery-ui.min.js"></script>
<!-- <script src="https://cdn.socket.io/4.1.2/socket.io.min.js" integrity="sha384-toS6mmwu70G0fw54EGlWWeA4z3dyJ+dlXBtSURSKN4vyRFOcxd3Bzjj/AoOwY+Rg" crossorigin="anonymous"></script> -->
<script src="/static/js/upstream/socket.io.min.js"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/fomantic-ui/2.9.0/semantic.min.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/fomantic-ui/2.9.0/semantic.min.js"></script>
<!-- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/fomantic-ui/2.9.0/semantic.min.css"> -->
<link rel="stylesheet" href="/static/css/upstream/semantic.min.css">
<!-- <script src="https://cdnjs.cloudflare.com/ajax/libs/fomantic-ui/2.9.0/semantic.min.js"></script> -->
<script src="/static/js/upstream/semantic.min.js"></script>
<link rel="stylesheet" href="/static/css/index.css">
<link rel="stylesheet" href="/static/css/tabs.css">
@@ -17,6 +23,8 @@
<script type="text/javascript">
var initial_stats = {{ initial_stats|tojson|safe }};
var latitude = parseFloat('{{ latitude|safe }}');
var longitude = parseFloat('{{ longitude|safe }}');
var memory_chart = null
var message_chart = null
@@ -26,6 +34,19 @@
start_update();
init_chat();
reset_Tabs();
console.log("latitude", latitude);
console.log("longitude", longitude);
if (isNaN(latitude) || isNaN(longitude) && location.protocol != 'https:') {
// Have to disable the beacon button.
$('#send_beacon').prop('disabled', true);
}
$("#wipe_local").click(function() {
console.log('Wipe local storage');
localStorage.clear();
});
});
</script>
</head>
@@ -64,6 +85,7 @@
</div>
<input type="submit" name="submit" class="ui button" id="send_msg" value="Send" />
<button type="button" class="ui button" id="send_beacon" value="Send GPS Beacon">Send GPS Beacon</button>
<button type="button" class="ui button" id="wipe_local" value="wipe local storage">Wipe LocalStorage</button>
</form>
</div>
</div>
@@ -73,7 +95,6 @@
<div class="tab" id="callsignTabs"></div>
</div>
<div class="ten wide column ui raised segment" id="msgsTabsDiv" style="height:450px;padding:0px;">
&nbsp;
</div>
</div>
+22 -6
View File
@@ -1,13 +1,19 @@
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1">
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.1/jquery.min.js"></script>
<link rel="stylesheet" href="https://ajax.googleapis.com/ajax/libs/jqueryui/1.12.1/themes/smoothness/jquery-ui.css">
<script src="https://ajax.googleapis.com/ajax/libs/jqueryui/1.12.1/jquery-ui.min.js"></script>
<script src="https://cdn.socket.io/4.1.2/socket.io.min.js" integrity="sha384-toS6mmwu70G0fw54EGlWWeA4z3dyJ+dlXBtSURSKN4vyRFOcxd3Bzjj/AoOwY+Rg" crossorigin="anonymous"></script>
<!-- <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script> -->
<script src="/static/js/upstream/jquery.min.js"></script>
<!-- <link rel="stylesheet" href="https://ajax.googleapis.com/ajax/libs/jqueryui/1.12.1/themes/smoothness/jquery-ui.css"> -->
<link rel="stylesheet" href="/static/css/upstream/jquery-ui.css">
<!-- <script src="https://ajax.googleapis.com/ajax/libs/jqueryui/1.12.1/jquery-ui.min.js"></script> -->
<script src="/static/js/upstream/jquery-ui.min.js"></script>
<!-- <script src="https://cdn.socket.io/4.1.2/socket.io.min.js" integrity="sha384-toS6mmwu70G0fw54EGlWWeA4z3dyJ+dlXBtSURSKN4vyRFOcxd3Bzjj/AoOwY+Rg" crossorigin="anonymous"></script> -->
<script src="/static/js/upstream/socket.io.min.js"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/fomantic-ui/2.9.0/semantic.min.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/fomantic-ui/2.9.0/semantic.min.js"></script>
<!-- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/fomantic-ui/2.9.0/semantic.min.css"> -->
<link rel="stylesheet" href="/static/css/upstream/semantic.min.css">
<!-- <script src="https://cdnjs.cloudflare.com/ajax/libs/fomantic-ui/2.9.0/semantic.min.js"></script> -->
<script src="/static/js/upstream/semantic.min.js"></script>
<link rel="stylesheet" href="/static/css/index.css">
<script src="/static/js/main.js"></script>
@@ -16,6 +22,8 @@
<script type="text/javascript">
var initial_stats = {{ initial_stats|tojson|safe }};
var latitude = parseFloat('{{ latitude|safe }}');
var longitude = parseFloat('{{ longitude|safe }}');
var memory_chart = null
var message_chart = null
@@ -24,6 +32,14 @@
console.log(initial_stats);
start_update();
init_chat();
console.log("latitude", latitude);
console.log("longitude", longitude);
if (isNaN(latitude) || isNaN(longitude) && location.protocol != 'https:') {
// Have to disable the beacon button.
$('#send_beacon').prop('disabled', true);
}
});
</script>
</head>
+6 -1
View File
@@ -330,7 +330,12 @@ if __name__ == "__main__":
setup_logging(app, log_level)
sio.register_namespace(LoggingNamespace("/logs"))
CONF.log_opt_values(LOG, logging.DEBUG)
app.run(threaded=True, debug=True, port=CONF.admin.web_port)
app.run(
threaded=True,
debug=False,
port=CONF.admin.web_port,
host=CONF.admin.web_ip,
)
if __name__ == "uwsgi_file_aprsd_wsgi":
+27 -27
View File
@@ -4,7 +4,7 @@
#
# pip-compile --annotation-style=line dev-requirements.in
#
add-trailing-comma==3.0.0 # via gray
add-trailing-comma==3.0.1 # via gray
alabaster==0.7.13 # via sphinx
attrs==23.1.0 # via jsonschema, referencing
autoflake==1.5.3 # via gray
@@ -13,75 +13,75 @@ black==23.7.0 # via gray
build==0.10.0 # via pip-tools
cachetools==5.3.1 # via tox
certifi==2023.7.22 # via requests
cfgv==3.3.1 # via pre-commit
chardet==5.1.0 # via tox
cfgv==3.4.0 # via pre-commit
chardet==5.2.0 # via tox
charset-normalizer==3.2.0 # via requests
click==8.1.6 # via black, pip-tools
colorama==0.4.6 # via tox
commonmark==0.9.1 # via rich
configargparse==1.7 # via gray
coverage[toml]==7.2.7 # via pytest-cov
coverage[toml]==7.3.0 # via pytest-cov
distlib==0.3.7 # via virtualenv
docutils==0.20.1 # via sphinx
exceptiongroup==1.1.2 # via pytest
exceptiongroup==1.1.3 # via pytest
filelock==3.12.2 # via tox, virtualenv
fixit==0.1.4 # via gray
flake8==6.0.0 # via -r dev-requirements.in, fixit, pep8-naming
flake8==6.1.0 # via -r dev-requirements.in, fixit, pep8-naming
gray==0.13.0 # via -r dev-requirements.in
identify==2.5.26 # via pre-commit
idna==3.4 # via requests
imagesize==1.4.1 # via sphinx
importlib-resources==6.0.0 # via fixit
importlib-resources==6.0.1 # via fixit
iniconfig==2.0.0 # via pytest
isort==5.12.0 # via -r dev-requirements.in, gray
jinja2==3.1.2 # via sphinx
jsonschema==4.18.4 # via fixit
jsonschema==4.19.0 # via fixit
jsonschema-specifications==2023.7.1 # via jsonschema
libcst==1.0.1 # via fixit
markupsafe==2.1.3 # via jinja2
mccabe==0.7.0 # via flake8
mypy==1.4.1 # via -r dev-requirements.in
mypy==1.5.0 # via -r dev-requirements.in
mypy-extensions==1.0.0 # via black, mypy, typing-inspect
nodeenv==1.8.0 # via pre-commit
packaging==23.1 # via black, build, pyproject-api, pytest, sphinx, tox
pathspec==0.11.1 # via black
pathspec==0.11.2 # via black
pep8-naming==0.13.3 # via -r dev-requirements.in
pip-tools==7.1.0 # via -r dev-requirements.in
platformdirs==3.9.1 # via black, tox, virtualenv
pip-tools==7.3.0 # via -r dev-requirements.in
platformdirs==3.10.0 # via black, tox, virtualenv
pluggy==1.2.0 # via pytest, tox
pre-commit==3.3.3 # via -r dev-requirements.in
pycodestyle==2.10.0 # via flake8
pyflakes==3.0.1 # via autoflake, flake8
pygments==2.15.1 # via rich, sphinx
pycodestyle==2.11.0 # via flake8
pyflakes==3.1.0 # via autoflake, flake8
pygments==2.16.1 # via rich, sphinx
pyproject-api==1.5.3 # via tox
pyproject-hooks==1.0.0 # via build
pytest==7.4.0 # via -r dev-requirements.in, pytest-cov
pytest-cov==4.1.0 # via -r dev-requirements.in
pyupgrade==3.9.0 # via gray
pyupgrade==3.10.1 # via gray
pyyaml==6.0.1 # via fixit, libcst, pre-commit
referencing==0.30.0 # via jsonschema, jsonschema-specifications
referencing==0.30.2 # via jsonschema, jsonschema-specifications
requests==2.31.0 # via sphinx
rich==12.6.0 # via gray
rpds-py==0.9.2 # via jsonschema, referencing
snowballstemmer==2.2.0 # via sphinx
sphinx==7.0.1 # via -r dev-requirements.in
sphinxcontrib-applehelp==1.0.4 # via sphinx
sphinxcontrib-devhelp==1.0.2 # via sphinx
sphinxcontrib-htmlhelp==2.0.1 # via sphinx
sphinx==7.1.2 # via -r dev-requirements.in, sphinxcontrib-applehelp, sphinxcontrib-devhelp, sphinxcontrib-htmlhelp, sphinxcontrib-qthelp, sphinxcontrib-serializinghtml
sphinxcontrib-applehelp==1.0.7 # via sphinx
sphinxcontrib-devhelp==1.0.5 # via sphinx
sphinxcontrib-htmlhelp==2.0.4 # via sphinx
sphinxcontrib-jsmath==1.0.1 # via sphinx
sphinxcontrib-qthelp==1.0.3 # via sphinx
sphinxcontrib-serializinghtml==1.1.5 # via sphinx
tokenize-rt==5.1.0 # via add-trailing-comma, pyupgrade
sphinxcontrib-qthelp==1.0.6 # via sphinx
sphinxcontrib-serializinghtml==1.1.8 # via sphinx
tokenize-rt==5.2.0 # via add-trailing-comma, pyupgrade
toml==0.10.2 # via autoflake
tomli==2.0.1 # via black, build, coverage, mypy, pip-tools, pyproject-api, pyproject-hooks, pytest, tox
tox==4.6.4 # via -r dev-requirements.in
tox==4.8.0 # via -r dev-requirements.in
typing-extensions==4.7.1 # via libcst, mypy, typing-inspect
typing-inspect==0.9.0 # via libcst
unify==0.5 # via gray
untokenize==0.1.1 # via unify
urllib3==2.0.4 # via requests
virtualenv==20.24.1 # via pre-commit, tox
wheel==0.41.0 # via pip-tools
virtualenv==20.24.3 # via pre-commit, tox
wheel==0.41.1 # via pip-tools
# The following packages are considered to be unsafe in a requirements file:
# pip
+44 -45
View File
@@ -1,62 +1,61 @@
#FROM python:3-bullseye as aprsd
FROM ubuntu:22.04 as aprsd
FROM python:3.11-slim as build
# Dockerfile for building a container during aprsd development.
ARG UID
ARG GID
ARG TZ
ARG VERSION=3.0.3
ARG BUILDX_QEMU_ENV
ENV APRS_USER=aprs
ENV HOME=/home/aprs
ARG VERSION=3.1.0
ENV TZ=${TZ:-US/Eastern}
ENV UID=${UID:-1000}
ENV GID=${GID:-1000}
ENV LC_ALL=C.UTF-8
ENV LANG=C.UTF-8
ENV APRSD_PIP_VERSION=${VERSION}
ENV DEBIAN_FRONTEND=noninteractive
RUN apt update
RUN apt install -y git build-essential
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 python3-setuptools-rust
RUN apt install -y libffi-dev cargo pkg-config
RUN pip3 install -U pip
RUN pip3 install -U setuptools_rust
ENV PIP_DEFAULT_TIMEOUT=100 \
# Allow statements and log messages to immediately appear
PYTHONUNBUFFERED=1 \
# disable a pip version check to reduce run-time & log-spam
PIP_DISABLE_PIP_VERSION_CHECK=1 \
# cache is useless in docker image, so disable to reduce image size
PIP_NO_CACHE_DIR=1
RUN addgroup --gid $GID $APRS_USER
RUN useradd -m -u $UID -g $APRS_USER $APRS_USER
RUN set -ex \
# Create a non-root user
&& addgroup --system --gid 1001 appgroup \
&& useradd --uid 1001 --gid 1001 -s /usr/bin/bash -m -d /app appuser \
# Upgrade the package index and install security upgrades
&& apt-get update \
&& apt-get upgrade -y \
&& apt-get install -y git build-essential curl libffi-dev \
python3-dev libssl-dev libxml2-dev libxslt-dev telnet sudo \
# Install dependencies
# Clean up
&& apt-get autoremove -y \
&& apt-get clean -y
# Handle an extremely specific issue when building the cryptography package for
# 32-bit architectures within QEMU running on a 64-bit host (issue #30).
RUN if [ "${BUILDX_QEMU_ENV}" = "true" -a "$(getconf LONG_BIT)" = "32" ]; then \
pip3 install -U cryptography==3.3.2; \
else \
pip3 install cryptography ;\
fi
# Ensure /config is there with a default config file
USER root
# Install aprsd
### Final stage
FROM build as final
WORKDIR /app
RUN pip3 install aprsd==$APRSD_PIP_VERSION
RUN mkdir -p /config
RUN pip install gevent uwsgi
RUN which aprsd
RUN mkdir /config
RUN chown -R appuser:appgroup /app
RUN chown -R appuser:appgroup /config
USER appuser
RUN which aprsd
RUN aprsd sample-config > /config/aprsd.conf
RUN chown -R $APRS_USER:$APRS_USER /config
RUN chown -R $APRS_USER:$APRS_USER $HOME
# override this to run another configuration
ENV CONF default
VOLUME ["/config", "/plugins"]
ADD bin/run.sh /app
ADD bin/listen.sh /app
ADD bin/admin.sh /app
USER $APRS_USER
ADD bin/run.sh /usr/local/bin
ADD bin/listen.sh /usr/local/bin
ENTRYPOINT ["/usr/local/bin/run.sh"]
# For the web admin interface
EXPOSE 8001
ENTRYPOINT ["/app/run.sh"]
VOLUME ["/config"]
# Set the user to run the application
USER appuser
HEALTHCHECK --interval=5m --timeout=12s --start-period=30s \
CMD aprsd healthcheck --config /config/aprsd.conf
+1 -1
View File
@@ -20,7 +20,7 @@ RUN set -ex \
# Upgrade the package index and install security upgrades
&& apt-get update \
&& apt-get upgrade -y \
&& apt-get install -y git build-essential curl vim libffi-dev \
&& apt-get install -y git build-essential curl libffi-dev \
python3-dev libssl-dev libxml2-dev libxslt-dev telnet sudo \
# Install dependencies
# Clean up
+8
View File
@@ -20,6 +20,14 @@ aprsd.cmds.dev module
:undoc-members:
:show-inheritance:
aprsd.cmds.fetch\_stats module
------------------------------
.. automodule:: aprsd.cmds.fetch_stats
:members:
:undoc-members:
:show-inheritance:
aprsd.cmds.healthcheck module
-----------------------------
-29
View File
@@ -1,29 +0,0 @@
aprsd.logging package
=====================
Submodules
----------
aprsd.logging.log module
------------------------
.. automodule:: aprsd.logging.log
:members:
:undoc-members:
:show-inheritance:
aprsd.logging.rich module
-------------------------
.. automodule:: aprsd.logging.rich
:members:
:undoc-members:
:show-inheritance:
Module contents
---------------
.. automodule:: aprsd.logging
:members:
:undoc-members:
:show-inheritance:
+12 -12
View File
@@ -10,7 +10,7 @@ Subpackages
aprsd.clients
aprsd.cmds
aprsd.conf
aprsd.logging
aprsd.log
aprsd.packets
aprsd.plugins
aprsd.rpc
@@ -21,14 +21,6 @@ Subpackages
Submodules
----------
aprsd.aprsd module
------------------
.. automodule:: aprsd.aprsd
:members:
:undoc-members:
:show-inheritance:
aprsd.cli\_helper module
------------------------
@@ -53,10 +45,10 @@ aprsd.exception module
:undoc-members:
:show-inheritance:
aprsd.flask module
------------------
aprsd.main module
-----------------
.. automodule:: aprsd.flask
.. automodule:: aprsd.main
:members:
:undoc-members:
:show-inheritance:
@@ -93,6 +85,14 @@ aprsd.stats module
:undoc-members:
:show-inheritance:
aprsd.wsgi module
-----------------
.. automodule:: aprsd.wsgi
:members:
:undoc-members:
:show-inheritance:
Module contents
---------------
-2
View File
@@ -29,14 +29,12 @@ kiss3
attrs
# for mobile checking
user-agents
pyopenssl
dataclasses
dacite2
oslo.config
rpyc
# Pin this here so it doesn't require a compile on
# raspi
cryptography
shellingham
geopy
rush
+7 -16
View File
@@ -4,39 +4,33 @@
#
# pip-compile --annotation-style=line requirements.in
#
anyio==3.7.1 # via httpcore
aprslib==0.7.2 # via -r requirements.in
attrs==23.1.0 # via -r requirements.in, ax253, kiss3, rush
ax253==0.1.5.post1 # via kiss3
beautifulsoup4==4.12.2 # via -r requirements.in
bidict==0.22.1 # via python-socketio
bitarray==2.8.0 # via ax253, kiss3
bitarray==2.8.1 # via ax253, kiss3
blinker==1.6.2 # via flask
certifi==2023.7.22 # via httpcore, requests
cffi==1.15.1 # via cryptography
certifi==2023.7.22 # via requests
charset-normalizer==3.2.0 # via requests
click==8.1.6 # via -r requirements.in, click-completion, click-params, flask
click-completion==0.5.2 # via -r requirements.in
click-params==0.4.1 # via -r requirements.in
commonmark==0.9.1 # via rich
cryptography==41.0.2 # via -r requirements.in, pyopenssl
dacite2==2.0.0 # via -r requirements.in
dataclasses==0.6 # via -r requirements.in
debtcollector==2.5.0 # via oslo-config
decorator==5.1.1 # via validators
dnspython==2.4.0 # via eventlet
dnspython==2.4.2 # via eventlet
eventlet==0.33.3 # via -r requirements.in
exceptiongroup==1.1.2 # via anyio
flask==2.3.2 # via -r requirements.in, flask-httpauth, flask-socketio
flask-httpauth==4.8.0 # via -r requirements.in
flask-socketio==5.3.4 # via -r requirements.in
flask-socketio==5.3.5 # via -r requirements.in
geographiclib==2.0 # via geopy
geopy==2.3.0 # via -r requirements.in
gevent==23.7.0 # via -r requirements.in
greenlet==2.0.2 # via eventlet, gevent
h11==0.14.0 # via httpcore
httpcore==0.17.3 # via dnspython
idna==3.4 # via anyio, requests
idna==3.4 # via requests
imapclient==2.3.1 # via -r requirements.in
importlib-metadata==6.8.0 # via ax253, kiss3
itsdangerous==2.1.2 # via flask
@@ -49,9 +43,7 @@ oslo-i18n==6.0.0 # via oslo-config
pbr==5.11.1 # via -r requirements.in, oslo-i18n, stevedore
pluggy==1.2.0 # via -r requirements.in
plumbum==1.8.2 # via rpyc
pycparser==2.21 # via cffi
pygments==2.15.1 # via rich
pyopenssl==23.2.0 # via -r requirements.in
pygments==2.16.1 # via rich
pyserial==3.5 # via pyserial-asyncio
pyserial-asyncio==0.6 # via kiss3
python-engineio==4.5.1 # via python-socketio
@@ -65,7 +57,6 @@ rpyc==5.3.1 # via -r requirements.in
rush==2021.4.0 # via -r requirements.in
shellingham==1.5.0.post1 # via -r requirements.in, click-completion
six==1.16.0 # via -r requirements.in, click-completion, eventlet, imapclient
sniffio==1.3.0 # via anyio, dnspython, httpcore
soupsieve==2.4.1 # via beautifulsoup4
stevedore==5.1.0 # via oslo-config
tabulate==0.9.0 # via -r requirements.in
@@ -75,7 +66,7 @@ update-checker==0.18.0 # via -r requirements.in
urllib3==2.0.4 # via requests
user-agents==2.2.0 # via -r requirements.in
validators==0.20.0 # via click-params
werkzeug==2.3.6 # via -r requirements.in, flask
werkzeug==2.3.7 # via -r requirements.in, flask
wrapt==1.15.0 # via -r requirements.in, debtcollector
zipp==3.16.2 # via importlib-metadata
zope-event==5.0 # via gevent