mirror of
https://github.com/craigerl/aprsd.git
synced 2026-08-16 16:43:52 -04:00
Compare commits
84 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| acf2b62bce | |||
| 8e9a0213e9 | |||
| bf905a0e9f | |||
| 5ae45ce42f | |||
| 0155923341 | |||
| 156d9d9592 | |||
| 81169600bd | |||
| 746eeb81b0 | |||
| f41488b48a | |||
| 116f201394 | |||
| ddd4d25e9d | |||
| e2f89a6043 | |||
| 544600a96b | |||
| c16f3a0bb2 | |||
| 59cec1317d | |||
| 751bbc2514 | |||
| 9bdfd166fd | |||
| f79b88ec1b | |||
| 99a0f877f4 | |||
| 4f87d5da12 | |||
| 0d7e50d2ba | |||
| 1f6c55d2bf | |||
| 740889426a | |||
| c9dc4f67d4 | |||
| 788a72c643 | |||
| 1e3d0d4faf | |||
| 82d25915fc | |||
| 12dfdefb62 | |||
| d63c6854af | |||
| 6b083d4c4d | |||
| ff358987a9 | |||
| 412ab54303 | |||
| 3f5dbe0a12 | |||
| 9635893934 | |||
| f151ae4348 | |||
| 7130ca2fd9 | |||
| b393060edb | |||
| f770c5ffd5 | |||
| ef206b1283 | |||
| 140fa4ace4 | |||
| 81a19dd101 | |||
| 9985c8bf25 | |||
| 1400e3e711 | |||
| 8a90d5480a | |||
| b4e02c760e | |||
| ba6b410795 | |||
| 70ddc44b5c | |||
| 852760220f | |||
| 14e984c9b4 | |||
| 29f21a9469 | |||
| 7292744a78 | |||
| 619b1b708e | |||
| 008b2ab09e | |||
| 4b56e99689 | |||
| 10bf04929e | |||
| a9e8050ae6 | |||
| 82f77b7a6a | |||
| 570fdb98a7 | |||
| 9582812041 | |||
| 859f904602 | |||
| 34311f0fbd | |||
| 2416f0ea1a | |||
| 377842c2ec | |||
| a8dd9ce012 | |||
| 1d6a667987 | |||
| 2e9a204c74 | |||
| f922b3f97b | |||
| 8dd3b05bb1 | |||
| e06305fceb | |||
| 33c7871dbe | |||
| b2f95b0f4e | |||
| ae9e4d31ad | |||
| 65a5a90458 | |||
| 182887c20a | |||
| f228144f4b | |||
| db9e1d23d1 | |||
| 986df391b2 | |||
| 3994235380 | |||
| 9ebf2f9a30 | |||
| 011cfc55e1 | |||
| e0c3c5cbbf | |||
| 26f354b3a9 | |||
| 922a6dbb35 | |||
| d03c4fc096 |
@@ -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 }}
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,9 +1,107 @@
|
||||
CHANGES
|
||||
=======
|
||||
|
||||
v3.2.1
|
||||
------
|
||||
|
||||
* Update index.html disable form autocomplete
|
||||
* Update the packet\_dupe\_timeout warning
|
||||
* Update the webchat paths
|
||||
* Changed the path option to a ListOpt
|
||||
* Fixed default path for tcp\_kiss client
|
||||
* Set a default password for admin
|
||||
* Fix path for KISS clients
|
||||
* Added packet\_dupe\_timeout conf
|
||||
* Add ability to change path on every TX packet
|
||||
* Make Packet objects hashable
|
||||
* Bump urllib3 from 2.0.4 to 2.0.6
|
||||
* Don't process AckPackets as dupes
|
||||
* Fixed another msgNo int issue
|
||||
* Fixed issue with packet tracker and msgNO Counter
|
||||
* Fixed import of Mutablemapping
|
||||
* pep8 fixes
|
||||
* rewrote packet\_list and drop dupe packets
|
||||
* Log a warning on dupe
|
||||
* Fix for dupe packets
|
||||
|
||||
v3.2.0
|
||||
------
|
||||
|
||||
* Update Changelog for 3.2.0
|
||||
* minor cleanup prior to release
|
||||
* Webchat: fix input maxlength
|
||||
* WebChat: cleanup some console.logs
|
||||
* WebChat: flash a dupe message
|
||||
* Webchat: Fix issue accessing msg.id
|
||||
* Webchat: Fix chat css on older browsers
|
||||
* WebChat: new tab should get focus
|
||||
* Bump gevent from 23.9.0.post1 to 23.9.1
|
||||
* Webchat: Fix pep8 errors
|
||||
* Webchat: Added tab notifications and raw packet
|
||||
* WebChat: Prevent sending message without callsign
|
||||
* WebChat: fixed content area scrolling
|
||||
* Webchat: tweaks to UI for expanding chat
|
||||
* Webchat: Fixed bug deleteing first tab
|
||||
* Ensure Keepalive doesn't reset client at startup
|
||||
* Ensure parse\_delta\_str doesn't puke
|
||||
* WebChat: Send GPS Beacon working
|
||||
* webchat: got active tab onclick working
|
||||
* webchat: set to\_call to value of tab when selected
|
||||
* Center the webchat input form
|
||||
* Update index.html to use chat.css
|
||||
* Deleted webchat mobile pages
|
||||
* Added close X on webchat tabs
|
||||
* Reworked webchat with new UI
|
||||
* Updated the webchat UI to look like iMessage
|
||||
* Restore previous conversations in webchat
|
||||
* Remove VIM from Dockerfile
|
||||
* recreate client during reset()
|
||||
* updated github workflows
|
||||
* Updated documentation build
|
||||
* 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
|
||||
|
||||
@@ -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()
|
||||
+53
-8
@@ -7,7 +7,7 @@ from aprslib.exceptions import LoginError
|
||||
from oslo_config import cfg
|
||||
|
||||
from aprsd import exception
|
||||
from aprsd.clients import aprsis, kiss
|
||||
from aprsd.clients import aprsis, fake, kiss
|
||||
from aprsd.packets import core, packet_list
|
||||
from aprsd.utils import trace
|
||||
|
||||
@@ -17,6 +17,7 @@ LOG = logging.getLogger("APRSD")
|
||||
TRANSPORT_APRSIS = "aprsis"
|
||||
TRANSPORT_TCPKISS = "tcpkiss"
|
||||
TRANSPORT_SERIALKISS = "serialkiss"
|
||||
TRANSPORT_FAKE = "fake"
|
||||
|
||||
# Main must create this from the ClientFactory
|
||||
# object such that it's populated with the
|
||||
@@ -24,7 +25,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 +50,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 +68,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 +90,7 @@ class Client:
|
||||
pass
|
||||
|
||||
|
||||
class APRSISClient(Client):
|
||||
class APRSISClient(Client, metaclass=trace.TraceWrapperMetaclass):
|
||||
|
||||
_client = None
|
||||
|
||||
@@ -133,7 +139,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 +164,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,14 +238,46 @@ 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
|
||||
|
||||
|
||||
class APRSDFakeClient(Client, metaclass=trace.TraceWrapperMetaclass):
|
||||
|
||||
@staticmethod
|
||||
def is_enabled():
|
||||
if CONF.fake_client.enabled:
|
||||
return True
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def is_configured():
|
||||
return APRSDFakeClient.is_enabled()
|
||||
|
||||
def is_alive(self):
|
||||
return True
|
||||
|
||||
def setup_connection(self):
|
||||
return fake.APRSDFakeClient()
|
||||
|
||||
@staticmethod
|
||||
def transport():
|
||||
return TRANSPORT_FAKE
|
||||
|
||||
def decode_packet(self, *args, **kwargs):
|
||||
LOG.debug(f"kwargs {kwargs}")
|
||||
pkt = kwargs["packet"]
|
||||
LOG.debug(f"Got an APRS Fake Packet '{pkt}'")
|
||||
return pkt
|
||||
|
||||
|
||||
class ClientFactory:
|
||||
_instance = None
|
||||
|
||||
@@ -259,8 +300,11 @@ class ClientFactory:
|
||||
key = TRANSPORT_APRSIS
|
||||
elif KISSClient.is_enabled():
|
||||
key = KISSClient.transport()
|
||||
elif APRSDFakeClient.is_enabled():
|
||||
key = TRANSPORT_FAKE
|
||||
|
||||
builder = self._builders.get(key)
|
||||
LOG.debug(f"Creating client {key}")
|
||||
if not builder:
|
||||
raise ValueError(key)
|
||||
return builder()
|
||||
@@ -301,3 +345,4 @@ class ClientFactory:
|
||||
factory.register(TRANSPORT_APRSIS, APRSISClient)
|
||||
factory.register(TRANSPORT_TCPKISS, KISSClient)
|
||||
factory.register(TRANSPORT_SERIALKISS, KISSClient)
|
||||
factory.register(TRANSPORT_FAKE, APRSDFakeClient)
|
||||
|
||||
@@ -106,7 +106,7 @@ class Aprsdis(aprslib.IS):
|
||||
aprsd.__version__,
|
||||
)
|
||||
|
||||
self.logger.info("Sending login information")
|
||||
self.logger.debug("Sending login information")
|
||||
|
||||
try:
|
||||
self._sendall(login_str)
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
import logging
|
||||
import threading
|
||||
import time
|
||||
|
||||
import aprslib
|
||||
from oslo_config import cfg
|
||||
import wrapt
|
||||
|
||||
from aprsd import conf # noqa
|
||||
from aprsd.packets import core
|
||||
from aprsd.utils import trace
|
||||
|
||||
|
||||
CONF = cfg.CONF
|
||||
LOG = logging.getLogger("APRSD")
|
||||
|
||||
|
||||
class APRSDFakeClient(metaclass=trace.TraceWrapperMetaclass):
|
||||
'''Fake client for testing.'''
|
||||
|
||||
# flag to tell us to stop
|
||||
thread_stop = False
|
||||
|
||||
lock = threading.Lock()
|
||||
path = []
|
||||
|
||||
def __init__(self):
|
||||
LOG.info("Starting APRSDFakeClient client.")
|
||||
self.path = ["WIDE1-1", "WIDE2-1"]
|
||||
|
||||
def stop(self):
|
||||
self.thread_stop = True
|
||||
LOG.info("Shutdown APRSDFakeClient client.")
|
||||
|
||||
def is_alive(self):
|
||||
"""If the connection is alive or not."""
|
||||
return not self.thread_stop
|
||||
|
||||
@wrapt.synchronized(lock)
|
||||
def send(self, packet: core.Packet):
|
||||
"""Send an APRS Message object."""
|
||||
LOG.info(f"Sending packet: {packet}")
|
||||
payload = None
|
||||
if isinstance(packet, core.Packet):
|
||||
packet.prepare()
|
||||
payload = packet.payload.encode("US-ASCII")
|
||||
if packet.path:
|
||||
packet.path
|
||||
else:
|
||||
self.path
|
||||
else:
|
||||
msg_payload = f"{packet.raw}{{{str(packet.msgNo)}"
|
||||
payload = (
|
||||
":{:<9}:{}".format(
|
||||
packet.to_call,
|
||||
msg_payload,
|
||||
)
|
||||
).encode("US-ASCII")
|
||||
|
||||
LOG.debug(
|
||||
f"FAKE::Send '{payload}' TO '{packet.to_call}' From "
|
||||
f"'{packet.from_call}' with PATH \"{self.path}\"",
|
||||
)
|
||||
|
||||
def consumer(self, callback, blocking=False, immortal=False, raw=False):
|
||||
LOG.debug("Start non blocking FAKE consumer")
|
||||
# Generate packets here?
|
||||
raw = "GTOWN>APDW16,WIDE1-1,WIDE2-1:}KM6LYW-9>APZ100,TCPIP,GTOWN*::KM6LYW :KM6LYW: 19 Miles SW"
|
||||
pkt_raw = aprslib.parse(raw)
|
||||
pkt = core.Packet.factory(pkt_raw)
|
||||
callback(packet=pkt)
|
||||
LOG.debug(f"END blocking FAKE consumer {self}")
|
||||
time.sleep(8)
|
||||
+16
-16
@@ -14,6 +14,8 @@ LOG = logging.getLogger("APRSD")
|
||||
|
||||
|
||||
class KISS3Client:
|
||||
path = []
|
||||
|
||||
def __init__(self):
|
||||
self.setup()
|
||||
|
||||
@@ -34,6 +36,7 @@ class KISS3Client:
|
||||
speed=CONF.kiss_serial.baudrate,
|
||||
strip_df_start=True,
|
||||
)
|
||||
self.path = CONF.kiss_serial.path
|
||||
elif CONF.kiss_tcp.enabled:
|
||||
LOG.debug(
|
||||
"KISS({}) TCP Connection to {}:{}".format(
|
||||
@@ -47,6 +50,7 @@ class KISS3Client:
|
||||
port=CONF.kiss_tcp.port,
|
||||
strip_df_start=True,
|
||||
)
|
||||
self.path = CONF.kiss_tcp.path
|
||||
|
||||
LOG.debug("Starting KISS interface connection")
|
||||
self.kiss.start()
|
||||
@@ -81,27 +85,20 @@ 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"]
|
||||
path = self.path
|
||||
if isinstance(packet, core.Packet):
|
||||
packet.prepare()
|
||||
payload = packet.payload.encode("US-ASCII")
|
||||
if packet.path:
|
||||
path = packet.path
|
||||
else:
|
||||
msg_payload = f"{packet.raw}{{{str(packet.msgNo)}"
|
||||
|
||||
if not payload:
|
||||
payload = (
|
||||
":{:<9}:{}".format(
|
||||
packet.to_call,
|
||||
@@ -109,9 +106,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,
|
||||
|
||||
@@ -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()
|
||||
|
||||
+67
-80
@@ -15,7 +15,6 @@ from flask.logging import default_handler
|
||||
from flask_httpauth import HTTPBasicAuth
|
||||
from flask_socketio import Namespace, SocketIO
|
||||
from oslo_config import cfg
|
||||
from user_agents import parse as ua_parse
|
||||
from werkzeug.security import check_password_hash, generate_password_hash
|
||||
import wrapt
|
||||
|
||||
@@ -24,7 +23,7 @@ from aprsd import cli_helper, client, conf, packets, stats, threads, utils
|
||||
from aprsd.log import rich as aprsd_logging
|
||||
from aprsd.main import cli
|
||||
from aprsd.threads import rx, tx
|
||||
from aprsd.utils import objectstore, trace
|
||||
from aprsd.utils import trace
|
||||
|
||||
|
||||
CONF = cfg.CONF
|
||||
@@ -58,7 +57,8 @@ def signal_handler(sig, frame):
|
||||
signal.signal(signal.SIGTERM, sys.exit(0))
|
||||
|
||||
|
||||
class SentMessages(objectstore.ObjectStoreMixin):
|
||||
class SentMessages:
|
||||
|
||||
_instance = None
|
||||
lock = threading.Lock()
|
||||
|
||||
@@ -75,25 +75,7 @@ class SentMessages(objectstore.ObjectStoreMixin):
|
||||
|
||||
@wrapt.synchronized(lock)
|
||||
def add(self, msg):
|
||||
self.data[msg.msgNo] = self.create(msg.msgNo)
|
||||
self.data[msg.msgNo]["from"] = msg.from_call
|
||||
self.data[msg.msgNo]["to"] = msg.to_call
|
||||
self.data[msg.msgNo]["message"] = msg.message_text.rstrip("\n")
|
||||
self.data[msg.msgNo]["raw"] = msg.message_text.rstrip("\n")
|
||||
|
||||
def create(self, id):
|
||||
return {
|
||||
"id": id,
|
||||
"ts": time.time(),
|
||||
"ack": False,
|
||||
"from": None,
|
||||
"to": None,
|
||||
"raw": None,
|
||||
"message": None,
|
||||
"status": None,
|
||||
"last_update": None,
|
||||
"reply": None,
|
||||
}
|
||||
self.data[msg.msgNo] = msg.__dict__
|
||||
|
||||
@wrapt.synchronized(lock)
|
||||
def __len__(self):
|
||||
@@ -149,33 +131,17 @@ class WebChatProcessPacketThread(rx.APRSDProcessPacketThread):
|
||||
def process_ack_packet(self, packet: packets.AckPacket):
|
||||
super().process_ack_packet(packet)
|
||||
ack_num = packet.get("msgNo")
|
||||
SentMessages().ack(int(ack_num))
|
||||
SentMessages().ack(ack_num)
|
||||
self.socketio.emit(
|
||||
"ack", SentMessages().get(int(ack_num)),
|
||||
"ack", SentMessages().get(ack_num),
|
||||
namespace="/sendmsg",
|
||||
)
|
||||
self.got_ack = True
|
||||
|
||||
def process_our_message_packet(self, packet: packets.MessagePacket):
|
||||
LOG.info(f"process MessagePacket {repr(packet)}")
|
||||
packet.get("addresse", None)
|
||||
fromcall = packet.from_call
|
||||
|
||||
message = packet.get("message_text", None)
|
||||
msg = {
|
||||
"id": 0,
|
||||
"ts": packet.get("timestamp", time.time()),
|
||||
"ack": False,
|
||||
"from": fromcall,
|
||||
"to": packet.to_call,
|
||||
"raw": packet.raw,
|
||||
"message": message,
|
||||
"status": None,
|
||||
"last_update": None,
|
||||
"reply": None,
|
||||
}
|
||||
self.socketio.emit(
|
||||
"new", msg,
|
||||
"new", packet.__dict__,
|
||||
namespace="/sendmsg",
|
||||
)
|
||||
|
||||
@@ -191,25 +157,26 @@ def _get_transport(stats):
|
||||
"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.is_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:
|
||||
# for pep8 violation
|
||||
aprs_connection = (
|
||||
"SerialKISS://{}@{} baud".format(
|
||||
CONF.kiss_serial.device,
|
||||
CONF.kiss_serial.baudrate,
|
||||
),
|
||||
elif client.KISSClient.is_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:
|
||||
# for pep8 violation
|
||||
aprs_connection = (
|
||||
"SerialKISS://{}@{} baud".format(
|
||||
CONF.kiss_serial.device,
|
||||
CONF.kiss_serial.baudrate,
|
||||
),
|
||||
)
|
||||
elif CONF.fake_client.enabled:
|
||||
transport = client.TRANSPORT_FAKE
|
||||
aprs_connection = "Fake Client"
|
||||
|
||||
return transport, aprs_connection
|
||||
|
||||
@@ -217,20 +184,10 @@ def _get_transport(stats):
|
||||
@auth.login_required
|
||||
@flask_app.route("/")
|
||||
def index():
|
||||
ua_str = request.headers.get("User-Agent")
|
||||
# this takes about 2 seconds :(
|
||||
user_agent = ua_parse(ua_str)
|
||||
LOG.debug(f"Is mobile? {user_agent.is_mobile}")
|
||||
stats = _stats()
|
||||
|
||||
if user_agent.is_mobile:
|
||||
html_template = "mobile.html"
|
||||
else:
|
||||
html_template = "index.html"
|
||||
|
||||
# For development
|
||||
# html_template = "mobile.html"
|
||||
|
||||
html_template = "index.html"
|
||||
LOG.debug(f"Template {html_template}")
|
||||
|
||||
transport, aprs_connection = _get_transport(stats)
|
||||
@@ -239,6 +196,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 +210,8 @@ def index():
|
||||
aprs_connection=aprs_connection,
|
||||
callsign=CONF.callsign,
|
||||
version=aprsd.__version__,
|
||||
latitude=latitude,
|
||||
longitude=longitude,
|
||||
)
|
||||
|
||||
|
||||
@@ -265,8 +231,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"]
|
||||
@@ -310,11 +280,22 @@ class SendMessageNamespace(Namespace):
|
||||
LOG.debug(f"WS: on_send {data}")
|
||||
self.request = data
|
||||
data["from"] = CONF.callsign
|
||||
path = data.get("path", None)
|
||||
if not path:
|
||||
path = []
|
||||
elif "," in path:
|
||||
path_opts = path.split(",")
|
||||
path = [x.strip() for x in path_opts]
|
||||
else:
|
||||
path = [path]
|
||||
|
||||
pkt = packets.MessagePacket(
|
||||
from_call=data["from"],
|
||||
to_call=data["to"].upper(),
|
||||
message_text=data["message"],
|
||||
path=path,
|
||||
)
|
||||
pkt.prepare()
|
||||
self.msg = pkt
|
||||
msgs = SentMessages()
|
||||
msgs.add(pkt)
|
||||
@@ -373,7 +354,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 +404,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 +427,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 +447,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 +462,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.")
|
||||
|
||||
@@ -11,14 +11,21 @@ aprs_group = cfg.OptGroup(
|
||||
name="aprs_network",
|
||||
title="APRS-IS Network settings",
|
||||
)
|
||||
|
||||
kiss_serial_group = cfg.OptGroup(
|
||||
name="kiss_serial",
|
||||
title="KISS Serial device connection",
|
||||
)
|
||||
|
||||
kiss_tcp_group = cfg.OptGroup(
|
||||
name="kiss_tcp",
|
||||
title="KISS TCP/IP Device connection",
|
||||
)
|
||||
|
||||
fake_client_group = cfg.OptGroup(
|
||||
name="fake_client",
|
||||
title="Fake Client settings",
|
||||
)
|
||||
aprs_opts = [
|
||||
cfg.BoolOpt(
|
||||
"enabled",
|
||||
@@ -65,6 +72,11 @@ kiss_serial_opts = [
|
||||
default=9600,
|
||||
help="The Serial device baud rate for communication",
|
||||
),
|
||||
cfg.ListOpt(
|
||||
"path",
|
||||
default=["WIDE1-1", "WIDE2-1"],
|
||||
help="The APRS path to use for wide area coverage.",
|
||||
),
|
||||
]
|
||||
|
||||
kiss_tcp_opts = [
|
||||
@@ -82,6 +94,19 @@ kiss_tcp_opts = [
|
||||
default=8001,
|
||||
help="The KISS TCP/IP network port",
|
||||
),
|
||||
cfg.ListOpt(
|
||||
"path",
|
||||
default=["WIDE1-1", "WIDE2-1"],
|
||||
help="The APRS path to use for wide area coverage.",
|
||||
),
|
||||
]
|
||||
|
||||
fake_client_opts = [
|
||||
cfg.BoolOpt(
|
||||
"enabled",
|
||||
default=False,
|
||||
help="Enable fake client connection.",
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
@@ -93,10 +118,14 @@ def register_opts(config):
|
||||
config.register_opts(kiss_serial_opts, group=kiss_serial_group)
|
||||
config.register_opts(kiss_tcp_opts, group=kiss_tcp_group)
|
||||
|
||||
config.register_group(fake_client_group)
|
||||
config.register_opts(fake_client_opts, group=fake_client_group)
|
||||
|
||||
|
||||
def list_opts():
|
||||
return {
|
||||
aprs_group.name: aprs_opts,
|
||||
kiss_serial_group.name: kiss_serial_opts,
|
||||
kiss_tcp_group.name: kiss_tcp_opts,
|
||||
fake_client_group.name: fake_client_opts,
|
||||
}
|
||||
|
||||
@@ -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 = [
|
||||
@@ -61,6 +65,11 @@ aprsd_opts = [
|
||||
"2 means 1 packet every 2 seconds allowed."
|
||||
"5 means 1 pack packet every 5 seconds allowed",
|
||||
),
|
||||
cfg.IntOpt(
|
||||
"packet_dupe_timeout",
|
||||
default=60,
|
||||
help="The number of seconds before a packet is not considered a duplicate.",
|
||||
),
|
||||
]
|
||||
|
||||
watch_list_opts = [
|
||||
@@ -115,6 +124,7 @@ admin_opts = [
|
||||
),
|
||||
cfg.StrOpt(
|
||||
"password",
|
||||
default="password",
|
||||
secret=True,
|
||||
help="Admin interface password",
|
||||
),
|
||||
@@ -163,6 +173,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 +206,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 +216,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,
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from aprsd.packets.core import ( # noqa: F401
|
||||
AckPacket, GPSPacket, MessagePacket, MicEPacket, Packet, PathPacket,
|
||||
RejectPacket, StatusPacket, WeatherPacket,
|
||||
AckPacket, GPSPacket, MessagePacket, MicEPacket, Packet, RejectPacket,
|
||||
StatusPacket, WeatherPacket,
|
||||
)
|
||||
from aprsd.packets.packet_list import PacketList # noqa: F401
|
||||
from aprsd.packets.seen_list import SeenList # noqa: F401
|
||||
|
||||
+138
-84
@@ -25,10 +25,11 @@ PACKET_TYPE_OBJECT = "object"
|
||||
PACKET_TYPE_UNKNOWN = "unknown"
|
||||
PACKET_TYPE_STATUS = "status"
|
||||
PACKET_TYPE_BEACON = "beacon"
|
||||
PACKET_TYPE_THIRDPARTY = "thirdparty"
|
||||
PACKET_TYPE_UNCOMPRESSED = "uncompressed"
|
||||
|
||||
|
||||
def _int_timestamp():
|
||||
def _init_timestamp():
|
||||
"""Build a unix style timestamp integer"""
|
||||
return int(round(time.time()))
|
||||
|
||||
@@ -44,26 +45,30 @@ def _init_msgNo(): # noqa: N802
|
||||
return c.value
|
||||
|
||||
|
||||
@dataclass
|
||||
@dataclass(unsafe_hash=True)
|
||||
class Packet(metaclass=abc.ABCMeta):
|
||||
from_call: str
|
||||
to_call: str
|
||||
addresse: str = None
|
||||
format: str = None
|
||||
from_call: str = field(default=None)
|
||||
to_call: str = field(default=None)
|
||||
addresse: str = field(default=None)
|
||||
format: str = field(default=None)
|
||||
msgNo: str = field(default_factory=_init_msgNo) # noqa: N815
|
||||
packet_type: str = None
|
||||
timestamp: float = field(default_factory=_int_timestamp)
|
||||
packet_type: str = field(default=None)
|
||||
timestamp: float = field(default_factory=_init_timestamp, compare=False, hash=False)
|
||||
# Holds the raw text string to be sent over the wire
|
||||
# or holds the raw string from input packet
|
||||
raw: str = None
|
||||
raw_dict: dict = field(repr=False, default_factory=lambda: {})
|
||||
raw: str = field(default=None, compare=False, hash=False)
|
||||
raw_dict: dict = field(repr=False, default_factory=lambda: {}, compare=False, hash=False)
|
||||
# Built by calling prepare(). raw needs this built first.
|
||||
payload: str = field(default=None)
|
||||
|
||||
# Fields related to sending packets out
|
||||
send_count: int = field(repr=False, default=0)
|
||||
retry_count: int = field(repr=False, default=3)
|
||||
last_send_time: datetime.timedelta = field(repr=False, default=None)
|
||||
send_count: int = field(repr=False, default=0, compare=False, hash=False)
|
||||
retry_count: int = field(repr=False, default=3, compare=False, hash=False)
|
||||
last_send_time: datetime.timedelta = field(repr=False, default=None, compare=False, hash=False)
|
||||
# Do we allow this packet to be saved to send later?
|
||||
allow_delay: bool = field(repr=False, default=True)
|
||||
allow_delay: bool = field(repr=False, default=True, compare=False, hash=False)
|
||||
path: List[str] = field(default_factory=list, compare=False, hash=False)
|
||||
via: str = field(default=None, compare=False, hash=False)
|
||||
|
||||
def __post__init__(self):
|
||||
LOG.warning(f"POST INIT {self}")
|
||||
@@ -86,20 +91,39 @@ class Packet(metaclass=abc.ABCMeta):
|
||||
else:
|
||||
return default
|
||||
|
||||
@property
|
||||
def key(self):
|
||||
"""Build a key for finding this packet in a dict."""
|
||||
return f"{self.from_call}:{self.addresse}:{self.msgNo}"
|
||||
|
||||
def update_timestamp(self):
|
||||
self.timestamp = _int_timestamp()
|
||||
self.timestamp = _init_timestamp()
|
||||
|
||||
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 +142,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:
|
||||
@@ -228,52 +265,33 @@ class Packet(metaclass=abc.ABCMeta):
|
||||
return repr
|
||||
|
||||
|
||||
@dataclass
|
||||
class PathPacket(Packet):
|
||||
path: List[str] = field(default_factory=list)
|
||||
via: str = None
|
||||
|
||||
def _build_raw(self):
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
@dataclass
|
||||
class AckPacket(PathPacket):
|
||||
response: str = None
|
||||
@dataclass(unsafe_hash=True)
|
||||
class AckPacket(Packet):
|
||||
response: str = field(default=None)
|
||||
|
||||
def __post__init__(self):
|
||||
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
|
||||
class RejectPacket(PathPacket):
|
||||
response: str = None
|
||||
@dataclass(unsafe_hash=True)
|
||||
class RejectPacket(Packet):
|
||||
response: str = field(default=None)
|
||||
|
||||
def __post__init__(self):
|
||||
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
|
||||
class MessagePacket(PathPacket):
|
||||
message_text: str = None
|
||||
@dataclass(unsafe_hash=True)
|
||||
class MessagePacket(Packet):
|
||||
message_text: str = field(default=None)
|
||||
|
||||
def _filter_for_send(self) -> str:
|
||||
"""Filter and format message string for FCC."""
|
||||
@@ -285,34 +303,32 @@ 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),
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class StatusPacket(PathPacket):
|
||||
status: str = None
|
||||
messagecapable: bool = False
|
||||
comment: str = None
|
||||
@dataclass(unsafe_hash=True)
|
||||
class StatusPacket(Packet):
|
||||
status: str = field(default=None)
|
||||
messagecapable: bool = field(default=False)
|
||||
comment: str = field(default=None)
|
||||
|
||||
def _build_raw(self):
|
||||
def _build_payload(self):
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
@dataclass
|
||||
class GPSPacket(PathPacket):
|
||||
latitude: float = 0.00
|
||||
longitude: float = 0.00
|
||||
altitude: float = 0.00
|
||||
rng: float = 0.00
|
||||
posambiguity: int = 0
|
||||
comment: str = None
|
||||
@dataclass(unsafe_hash=True)
|
||||
class GPSPacket(Packet):
|
||||
latitude: float = field(default=0.00)
|
||||
longitude: float = field(default=0.00)
|
||||
altitude: float = field(default=0.00)
|
||||
rng: float = field(default=0.00)
|
||||
posambiguity: int = field(default=0)
|
||||
comment: str = field(default=None)
|
||||
symbol: str = field(default="l")
|
||||
symbol_table: str = field(default="/")
|
||||
|
||||
@@ -400,16 +416,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 +446,7 @@ class MicEPacket(GPSPacket):
|
||||
# 0 to 360
|
||||
course: int = 0
|
||||
|
||||
def _build_raw(self):
|
||||
def _build_payload(self):
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
@@ -436,6 +460,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 +483,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 +505,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 +533,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 +553,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 +591,7 @@ TYPE_LOOKUP = {
|
||||
PACKET_TYPE_STATUS: StatusPacket,
|
||||
PACKET_TYPE_BEACON: GPSPacket,
|
||||
PACKET_TYPE_UNKNOWN: Packet,
|
||||
PACKET_TYPE_THIRDPARTY: ThirdParty,
|
||||
}
|
||||
|
||||
|
||||
@@ -566,6 +618,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
|
||||
|
||||
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
from collections import OrderedDict
|
||||
from collections.abc import MutableMapping
|
||||
import logging
|
||||
import threading
|
||||
|
||||
from oslo_config import cfg
|
||||
import wrapt
|
||||
|
||||
from aprsd import stats, utils
|
||||
from aprsd import stats
|
||||
from aprsd.packets import seen_list
|
||||
|
||||
|
||||
@@ -12,31 +14,24 @@ CONF = cfg.CONF
|
||||
LOG = logging.getLogger("APRSD")
|
||||
|
||||
|
||||
class PacketList:
|
||||
"""Class to track all of the packets rx'd and tx'd by aprsd."""
|
||||
|
||||
class PacketList(MutableMapping):
|
||||
_instance = None
|
||||
lock = threading.Lock()
|
||||
|
||||
packet_list: utils.RingBuffer = utils.RingBuffer(1000)
|
||||
|
||||
_total_rx: int = 0
|
||||
_total_tx: int = 0
|
||||
|
||||
def __new__(cls, *args, **kwargs):
|
||||
if cls._instance is None:
|
||||
cls._instance = super().__new__(cls)
|
||||
cls._maxlen = 1000
|
||||
cls.d = OrderedDict()
|
||||
return cls._instance
|
||||
|
||||
@wrapt.synchronized(lock)
|
||||
def __iter__(self):
|
||||
return iter(self.packet_list)
|
||||
|
||||
@wrapt.synchronized(lock)
|
||||
def rx(self, packet):
|
||||
"""Add a packet that was received."""
|
||||
self._total_rx += 1
|
||||
self.packet_list.append(packet)
|
||||
self._add(packet)
|
||||
seen_list.SeenList().update_seen(packet)
|
||||
stats.APRSDStats().rx(packet)
|
||||
|
||||
@@ -44,13 +39,44 @@ class PacketList:
|
||||
def tx(self, packet):
|
||||
"""Add a packet that was received."""
|
||||
self._total_tx += 1
|
||||
self.packet_list.append(packet)
|
||||
self._add(packet)
|
||||
seen_list.SeenList().update_seen(packet)
|
||||
stats.APRSDStats().tx(packet)
|
||||
|
||||
@wrapt.synchronized(lock)
|
||||
def get(self):
|
||||
return self.packet_list.get()
|
||||
def add(self, packet):
|
||||
self._add(packet)
|
||||
|
||||
def _add(self, packet):
|
||||
self[packet.key] = packet
|
||||
|
||||
@property
|
||||
def maxlen(self):
|
||||
return self._maxlen
|
||||
|
||||
@wrapt.synchronized(lock)
|
||||
def find(self, packet):
|
||||
return self.get(packet.key)
|
||||
|
||||
def __getitem__(self, key):
|
||||
# self.d.move_to_end(key)
|
||||
return self.d[key]
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
if key in self.d:
|
||||
self.d.move_to_end(key)
|
||||
elif len(self.d) == self.maxlen:
|
||||
self.d.popitem(last=False)
|
||||
self.d[key] = value
|
||||
|
||||
def __delitem__(self, key):
|
||||
del self.d[key]
|
||||
|
||||
def __iter__(self):
|
||||
return self.d.__iter__()
|
||||
|
||||
def __len__(self):
|
||||
return len(self.d)
|
||||
|
||||
@wrapt.synchronized(lock)
|
||||
def total_rx(self):
|
||||
|
||||
@@ -62,30 +62,22 @@ class PacketTrack(objectstore.ObjectStoreMixin):
|
||||
def __len__(self):
|
||||
return len(self.data)
|
||||
|
||||
@wrapt.synchronized(lock)
|
||||
def __str__(self):
|
||||
result = "{"
|
||||
for key in self.data.keys():
|
||||
result += f"{key}: {str(self.data[key])}, "
|
||||
result += "}"
|
||||
return result
|
||||
|
||||
@wrapt.synchronized(lock)
|
||||
def add(self, packet):
|
||||
key = int(packet.msgNo)
|
||||
key = packet.msgNo
|
||||
self.data[key] = packet
|
||||
self.total_tracked += 1
|
||||
|
||||
@wrapt.synchronized(lock)
|
||||
def get(self, id):
|
||||
if id in self.data:
|
||||
return self.data[id]
|
||||
def get(self, key):
|
||||
return self.data.get(key, None)
|
||||
|
||||
@wrapt.synchronized(lock)
|
||||
def remove(self, id):
|
||||
key = int(id)
|
||||
if key in self.data.keys():
|
||||
def remove(self, key):
|
||||
try:
|
||||
del self.data[key]
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
def restart(self):
|
||||
"""Walk the list of messages and restart them if any."""
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
@@ -246,8 +246,6 @@ class APRSDStats:
|
||||
},
|
||||
"plugins": plugin_stats,
|
||||
}
|
||||
LOG.debug(f"STATS = {stats}")
|
||||
LOG.info("APRSD Stats: DONE")
|
||||
return stats
|
||||
|
||||
def __str__(self):
|
||||
|
||||
@@ -82,7 +82,12 @@ class KeepAliveThread(APRSDThread):
|
||||
|
||||
# check the APRS connection
|
||||
cl = client.factory.create()
|
||||
if not cl.is_alive():
|
||||
# Reset the connection if it's dead and this isn't our
|
||||
# First time through the loop.
|
||||
# The first time through the loop can happen at startup where
|
||||
# The keepalive thread starts before the client has a chance
|
||||
# to make it's connection the first time.
|
||||
if not cl.is_alive() and self.cntr > 0:
|
||||
LOG.error(f"{cl.__class__.__name__} is not alive!!! Resetting")
|
||||
client.factory.create().reset()
|
||||
else:
|
||||
|
||||
+47
-7
@@ -67,11 +67,55 @@ class APRSDPluginRXThread(APRSDRXThread):
|
||||
"""
|
||||
|
||||
def process_packet(self, *args, **kwargs):
|
||||
"""This handles the processing of an inbound packet.
|
||||
|
||||
When a packet is received by the connected client object,
|
||||
it sends the raw packet into this function. This function then
|
||||
decodes the packet via the client, and then processes the packet.
|
||||
Ack Packets are sent to the PluginProcessPacketThread for processing.
|
||||
All other packets have to be checked as a dupe, and then only after
|
||||
we haven't seen this packet before, do we send it to the
|
||||
PluginProcessPacketThread for processing.
|
||||
"""
|
||||
packet = self._client.decode_packet(*args, **kwargs)
|
||||
# LOG.debug(raw)
|
||||
packet.log(header="RX")
|
||||
packets.PacketList().rx(packet)
|
||||
self.packet_queue.put(packet)
|
||||
|
||||
if isinstance(packet, packets.AckPacket):
|
||||
# We don't need to drop AckPackets, those should be
|
||||
# processed.
|
||||
self.packet_queue.put(packet)
|
||||
else:
|
||||
# Make sure we aren't re-processing the same packet
|
||||
# For RF based APRS Clients we can get duplicate packets
|
||||
# So we need to track them and not process the dupes.
|
||||
found = False
|
||||
pkt_list = packets.PacketList()
|
||||
try:
|
||||
# Find the packet in the list of already seen packets
|
||||
# Based on the packet.key
|
||||
found = pkt_list.find(packet)
|
||||
except KeyError:
|
||||
found = False
|
||||
|
||||
if not found:
|
||||
# If we are in the process of already ack'ing
|
||||
# a packet, we should drop the packet
|
||||
# because it's a dupe within the time that
|
||||
# we send the 3 acks for the packet.
|
||||
pkt_list.rx(packet)
|
||||
self.packet_queue.put(packet)
|
||||
elif packet.timestamp - found.timestamp < CONF.packet_dupe_timeout:
|
||||
# If the packet came in within 60 seconds of the
|
||||
# Last time seeing the packet, then we drop it as a dupe.
|
||||
LOG.warning(f"Packet {packet.from_call}:{packet.msgNo} already tracked, dropping.")
|
||||
else:
|
||||
LOG.warning(
|
||||
f"Packet {packet.from_call}:{packet.msgNo} already tracked "
|
||||
f"but older than {CONF.packet_dupe_timeout} seconds. processing.",
|
||||
)
|
||||
pkt_list.rx(packet)
|
||||
self.packet_queue.put(packet)
|
||||
|
||||
|
||||
class APRSDProcessPacketThread(APRSDThread):
|
||||
@@ -125,8 +169,6 @@ class APRSDProcessPacketThread(APRSDThread):
|
||||
|
||||
# We don't put ack packets destined for us through the
|
||||
# plugins.
|
||||
wl = packets.WatchList()
|
||||
wl.update_seen(packet)
|
||||
if (
|
||||
isinstance(packet, packets.AckPacket)
|
||||
and packet.addresse.lower() == our_call
|
||||
@@ -214,9 +256,7 @@ class APRSDPluginProcessPacketThread(APRSDProcessPacketThread):
|
||||
to_call = packet.addresse
|
||||
else:
|
||||
to_call = None
|
||||
# msg = packet.get("message_text", None)
|
||||
# packet.get("msgNo", "0")
|
||||
# packet.get("response", None)
|
||||
|
||||
pm = plugin.PluginManager()
|
||||
try:
|
||||
results = pm.run(packet)
|
||||
|
||||
@@ -126,4 +126,8 @@ def parse_delta_str(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()}
|
||||
|
||||
if m:
|
||||
return {key: float(val) for key, val in m.groupdict().items()}
|
||||
else:
|
||||
return {}
|
||||
|
||||
@@ -37,7 +37,7 @@ class PacketCounter:
|
||||
@property
|
||||
@wrapt.synchronized(lock)
|
||||
def value(self):
|
||||
return self.val.value
|
||||
return str(self.val.value)
|
||||
|
||||
@wrapt.synchronized(lock)
|
||||
def __repr__(self):
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
input[type=search]::-webkit-search-cancel-button {
|
||||
-webkit-appearance: searchfield-cancel-button;
|
||||
}
|
||||
|
||||
.speech-wrapper {
|
||||
padding-top: 0px;
|
||||
padding: 5px 30px;
|
||||
background-color: #CCCCCC;
|
||||
}
|
||||
|
||||
.bubble-row {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.bubble-row.alt {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.bubble {
|
||||
/*width: 350px; */
|
||||
height: auto;
|
||||
display: block;
|
||||
background: #f5f5f5;
|
||||
border-radius: 4px;
|
||||
box-shadow: 2px 8px 5px #555;
|
||||
position: relative;
|
||||
margin: 0 0 15px;
|
||||
}
|
||||
|
||||
.bubble.alt {
|
||||
margin: 0 0 15px;
|
||||
}
|
||||
|
||||
.bubble-text {
|
||||
padding: 5px 5px 0px 8px;
|
||||
}
|
||||
|
||||
.bubble-name {
|
||||
width: 280px;
|
||||
font-weight: 600;
|
||||
font-size: 12px;
|
||||
margin: 0 0 0px;
|
||||
color: #3498db;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
.material-symbols-rounded {
|
||||
margin-left: auto;
|
||||
font-weight: normal;
|
||||
color: #808080;
|
||||
}
|
||||
}
|
||||
.bubble-name.alt {
|
||||
color: #2ecc71;
|
||||
}
|
||||
|
||||
.bubble-timestamp {
|
||||
margin-right: auto;
|
||||
font-size: 11px;
|
||||
text-transform: uppercase;
|
||||
color: #bbb
|
||||
}
|
||||
|
||||
.bubble-message {
|
||||
font-size: 16px;
|
||||
margin: 0px;
|
||||
padding: 0px 0px 0px 0px;
|
||||
color: #2b2b2b;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.bubble-arrow {
|
||||
position: absolute;
|
||||
width: 0;
|
||||
bottom:30px;
|
||||
left: -16px;
|
||||
height: 0px;
|
||||
}
|
||||
|
||||
.bubble-arrow.alt {
|
||||
right: -2px;
|
||||
bottom: 30px;
|
||||
left: auto;
|
||||
}
|
||||
|
||||
.bubble-arrow:after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
border: 0 solid transparent;
|
||||
border-top: 9px solid #f5f5f5;
|
||||
border-radius: 0 20px 0;
|
||||
width: 15px;
|
||||
height: 30px;
|
||||
transform: rotate(145deg);
|
||||
}
|
||||
.bubble-arrow.alt:after {
|
||||
transform: rotate(45deg) scaleY(-1);
|
||||
}
|
||||
|
||||
.popover {
|
||||
max-width: 400px;
|
||||
}
|
||||
.popover-header {
|
||||
font-size: 8pt;
|
||||
max-width: 400px;
|
||||
padding: 5px;
|
||||
background-color: #ee;
|
||||
}
|
||||
|
||||
.popover-body {
|
||||
white-space: pre-line;
|
||||
max-width: 400px;
|
||||
padding: 5px;
|
||||
}
|
||||
@@ -1,36 +1,9 @@
|
||||
body {
|
||||
background: #eeeeee;
|
||||
margin: 2em;
|
||||
/*margin: 1em;*/
|
||||
text-align: center;
|
||||
font-family: system-ui, sans-serif;
|
||||
}
|
||||
|
||||
footer {
|
||||
padding: 2em;
|
||||
text-align: center;
|
||||
height: 10vh;
|
||||
}
|
||||
|
||||
.ui.segment {
|
||||
background: #eeeeee;
|
||||
}
|
||||
|
||||
ul.list {
|
||||
list-style-type: disc;
|
||||
}
|
||||
ul.list li {
|
||||
list-style-position: outside;
|
||||
}
|
||||
|
||||
#left {
|
||||
margin-right: 2px;
|
||||
height: 300px;
|
||||
}
|
||||
#right {
|
||||
height: 300px;
|
||||
}
|
||||
#center {
|
||||
height: 300px;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
#title {
|
||||
@@ -39,6 +12,7 @@ ul.list li {
|
||||
#version{
|
||||
font-size: .5em;
|
||||
}
|
||||
|
||||
#uptime, #aprsis {
|
||||
font-size: 1em;
|
||||
}
|
||||
@@ -67,28 +41,26 @@ ul.list li {
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
#msgsTabsDiv .ui.tab {
|
||||
margin:0px;
|
||||
padding:0px;
|
||||
display: block;
|
||||
.wc-container {
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
height: 100%;
|
||||
}
|
||||
.wc-container .wc-row {
|
||||
/*border: 1px dotted #0313fc;*/
|
||||
padding: 2px;
|
||||
}
|
||||
.wc-container .wc-row.header {
|
||||
flex: 0 1 auto;
|
||||
}
|
||||
.wc-container .wc-row.content {
|
||||
flex: 1 1 auto;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.wc-container .wc-row.footer {
|
||||
flex: 0 1 0px;
|
||||
}
|
||||
|
||||
#msgsTabsDiv .header, .tiny.text, .content, .break,
|
||||
.thumbs.down.outline.icon,
|
||||
.phone.volume.icon
|
||||
{
|
||||
display: inline-block;
|
||||
float: left;
|
||||
position: relative;
|
||||
}
|
||||
#msgsTabsDiv .tiny.text {
|
||||
width:100px;
|
||||
}
|
||||
#msgsTabsDiv .tiny.header {
|
||||
width:100px;
|
||||
text-align: left;
|
||||
}
|
||||
#msgsTabsDiv .break {
|
||||
margin: 2px;
|
||||
text-align: left;
|
||||
.material-symbols-rounded.md-10 {
|
||||
font-size: 18px !important;
|
||||
}
|
||||
|
||||
@@ -37,5 +37,5 @@
|
||||
border: 1px solid #ccc;
|
||||
height: 450px;
|
||||
overflow-y: scroll;
|
||||
background-color: white;
|
||||
background-color: #CCCCCC;
|
||||
}
|
||||
|
||||
+1311
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,28 @@
|
||||
/**
|
||||
* jQuery toast plugin created by Kamran Ahmed copyright MIT license 2014
|
||||
*/
|
||||
.jq-toast-wrap { display: block; position: fixed; width: 250px; pointer-events: none !important; margin: 0; padding: 0; letter-spacing: normal; z-index: 9000 !important; }
|
||||
.jq-toast-wrap * { margin: 0; padding: 0; }
|
||||
|
||||
.jq-toast-wrap.bottom-left { bottom: 20px; left: 20px; }
|
||||
.jq-toast-wrap.bottom-right { bottom: 20px; right: 40px; }
|
||||
.jq-toast-wrap.top-left { top: 20px; left: 20px; }
|
||||
.jq-toast-wrap.top-right { top: 20px; right: 40px; }
|
||||
|
||||
.jq-toast-single { display: block; width: 100%; padding: 10px; margin: 0px 0px 5px; border-radius: 4px; font-size: 12px; font-family: arial, sans-serif; line-height: 17px; position: relative; pointer-events: all !important; background-color: #444444; color: white; }
|
||||
|
||||
.jq-toast-single h2 { font-family: arial, sans-serif; font-size: 14px; margin: 0px 0px 7px; background: none; color: inherit; line-height: inherit; letter-spacing: normal; }
|
||||
.jq-toast-single a { color: #eee; text-decoration: none; font-weight: bold; border-bottom: 1px solid white; padding-bottom: 3px; font-size: 12px; }
|
||||
|
||||
.jq-toast-single ul { margin: 0px 0px 0px 15px; background: none; padding:0px; }
|
||||
.jq-toast-single ul li { list-style-type: disc !important; line-height: 17px; background: none; margin: 0; padding: 0; letter-spacing: normal; }
|
||||
|
||||
.close-jq-toast-single { position: absolute; top: 3px; right: 7px; font-size: 14px; cursor: pointer; }
|
||||
|
||||
.jq-toast-loader { display: block; position: absolute; top: -2px; height: 5px; width: 0%; left: 0; border-radius: 5px; background: red; }
|
||||
.jq-toast-loaded { width: 100%; }
|
||||
.jq-has-icon { padding: 10px 10px 10px 50px; background-repeat: no-repeat; background-position: 10px; }
|
||||
.jq-icon-info { background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAGwSURBVEhLtZa9SgNBEMc9sUxxRcoUKSzSWIhXpFMhhYWFhaBg4yPYiWCXZxBLERsLRS3EQkEfwCKdjWJAwSKCgoKCcudv4O5YLrt7EzgXhiU3/4+b2ckmwVjJSpKkQ6wAi4gwhT+z3wRBcEz0yjSseUTrcRyfsHsXmD0AmbHOC9Ii8VImnuXBPglHpQ5wwSVM7sNnTG7Za4JwDdCjxyAiH3nyA2mtaTJufiDZ5dCaqlItILh1NHatfN5skvjx9Z38m69CgzuXmZgVrPIGE763Jx9qKsRozWYw6xOHdER+nn2KkO+Bb+UV5CBN6WC6QtBgbRVozrahAbmm6HtUsgtPC19tFdxXZYBOfkbmFJ1VaHA1VAHjd0pp70oTZzvR+EVrx2Ygfdsq6eu55BHYR8hlcki+n+kERUFG8BrA0BwjeAv2M8WLQBtcy+SD6fNsmnB3AlBLrgTtVW1c2QN4bVWLATaIS60J2Du5y1TiJgjSBvFVZgTmwCU+dAZFoPxGEEs8nyHC9Bwe2GvEJv2WXZb0vjdyFT4Cxk3e/kIqlOGoVLwwPevpYHT+00T+hWwXDf4AJAOUqWcDhbwAAAAASUVORK5CYII='); background-color: #31708f; color: #d9edf7; border-color: #bce8f1; }
|
||||
.jq-icon-warning { background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAGYSURBVEhL5ZSvTsNQFMbXZGICMYGYmJhAQIJAICYQPAACiSDB8AiICQQJT4CqQEwgJvYASAQCiZiYmJhAIBATCARJy+9rTsldd8sKu1M0+dLb057v6/lbq/2rK0mS/TRNj9cWNAKPYIJII7gIxCcQ51cvqID+GIEX8ASG4B1bK5gIZFeQfoJdEXOfgX4QAQg7kH2A65yQ87lyxb27sggkAzAuFhbbg1K2kgCkB1bVwyIR9m2L7PRPIhDUIXgGtyKw575yz3lTNs6X4JXnjV+LKM/m3MydnTbtOKIjtz6VhCBq4vSm3ncdrD2lk0VgUXSVKjVDJXJzijW1RQdsU7F77He8u68koNZTz8Oz5yGa6J3H3lZ0xYgXBK2QymlWWA+RWnYhskLBv2vmE+hBMCtbA7KX5drWyRT/2JsqZ2IvfB9Y4bWDNMFbJRFmC9E74SoS0CqulwjkC0+5bpcV1CZ8NMej4pjy0U+doDQsGyo1hzVJttIjhQ7GnBtRFN1UarUlH8F3xict+HY07rEzoUGPlWcjRFRr4/gChZgc3ZL2d8oAAAAASUVORK5CYII='); background-color: #8a6d3b; color: #fcf8e3; border-color: #faebcc; }
|
||||
.jq-icon-error { background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAHOSURBVEhLrZa/SgNBEMZzh0WKCClSCKaIYOED+AAKeQQLG8HWztLCImBrYadgIdY+gIKNYkBFSwu7CAoqCgkkoGBI/E28PdbLZmeDLgzZzcx83/zZ2SSXC1j9fr+I1Hq93g2yxH4iwM1vkoBWAdxCmpzTxfkN2RcyZNaHFIkSo10+8kgxkXIURV5HGxTmFuc75B2RfQkpxHG8aAgaAFa0tAHqYFfQ7Iwe2yhODk8+J4C7yAoRTWI3w/4klGRgR4lO7Rpn9+gvMyWp+uxFh8+H+ARlgN1nJuJuQAYvNkEnwGFck18Er4q3egEc/oO+mhLdKgRyhdNFiacC0rlOCbhNVz4H9FnAYgDBvU3QIioZlJFLJtsoHYRDfiZoUyIxqCtRpVlANq0EU4dApjrtgezPFad5S19Wgjkc0hNVnuF4HjVA6C7QrSIbylB+oZe3aHgBsqlNqKYH48jXyJKMuAbiyVJ8KzaB3eRc0pg9VwQ4niFryI68qiOi3AbjwdsfnAtk0bCjTLJKr6mrD9g8iq/S/B81hguOMlQTnVyG40wAcjnmgsCNESDrjme7wfftP4P7SP4N3CJZdvzoNyGq2c/HWOXJGsvVg+RA/k2MC/wN6I2YA2Pt8GkAAAAASUVORK5CYII='); background-color: #a94442; color: #f2dede; border-color: #ebccd1; }
|
||||
.jq-icon-success { background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAADsSURBVEhLY2AYBfQMgf///3P8+/evAIgvA/FsIF+BavYDDWMBGroaSMMBiE8VC7AZDrIFaMFnii3AZTjUgsUUWUDA8OdAH6iQbQEhw4HyGsPEcKBXBIC4ARhex4G4BsjmweU1soIFaGg/WtoFZRIZdEvIMhxkCCjXIVsATV6gFGACs4Rsw0EGgIIH3QJYJgHSARQZDrWAB+jawzgs+Q2UO49D7jnRSRGoEFRILcdmEMWGI0cm0JJ2QpYA1RDvcmzJEWhABhD/pqrL0S0CWuABKgnRki9lLseS7g2AlqwHWQSKH4oKLrILpRGhEQCw2LiRUIa4lwAAAABJRU5ErkJggg=='); color: #dff0d8; background-color: #3c763d; border-color: #d6e9c6; }
|
||||
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -62,10 +70,11 @@ function showPosition(position) {
|
||||
}
|
||||
console.log(msg);
|
||||
$.toast({
|
||||
title: 'Sending GPS Beacon',
|
||||
message: "Latitude: "+position.coords.latitude+"<br>Longitude: "+position.coords.longitude,
|
||||
showProgress: 'bottom',
|
||||
classProgress: 'red'
|
||||
heading: 'Sending GPS Beacon',
|
||||
text: "Latitude: "+position.coords.latitude+"<br>Longitude: "+position.coords.longitude,
|
||||
loader: true,
|
||||
loaderBg: '#9EC600',
|
||||
position: 'top-center',
|
||||
});
|
||||
|
||||
console.log("Sending GPS msg")
|
||||
|
||||
@@ -1,223 +0,0 @@
|
||||
var cleared = false;
|
||||
var callsign_list = {};
|
||||
var message_list = {};
|
||||
|
||||
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");
|
||||
});
|
||||
socket.on('connected', function(msg) {
|
||||
console.log("Connected!");
|
||||
console.log(msg);
|
||||
});
|
||||
|
||||
socket.on("sent", function(msg) {
|
||||
if (cleared == false) {
|
||||
var msgsdiv = $("#msgsTabsDiv");
|
||||
msgsdiv.html('')
|
||||
cleared = true
|
||||
}
|
||||
sent_msg(msg);
|
||||
});
|
||||
|
||||
socket.on("ack", function(msg) {
|
||||
update_msg(msg);
|
||||
});
|
||||
|
||||
socket.on("new", function(msg) {
|
||||
if (cleared == false) {
|
||||
var msgsdiv = $("#msgsTabsDiv");
|
||||
msgsdiv.html('')
|
||||
cleared = true
|
||||
}
|
||||
from_msg(msg);
|
||||
});
|
||||
|
||||
$("#sendform").submit(function(event) {
|
||||
event.preventDefault();
|
||||
msg = {'to': $('#to_call').val().toUpperCase(),
|
||||
'message': $('#message').val(),
|
||||
}
|
||||
socket.emit("send", msg);
|
||||
$('#message').val('');
|
||||
$('#to_call').val('');
|
||||
});
|
||||
|
||||
init_gps();
|
||||
}
|
||||
|
||||
|
||||
function add_callsign(callsign) {
|
||||
/* Ensure a callsign exists in the left hand nav */
|
||||
dropdown = $('#callsign_dropdown')
|
||||
|
||||
if (callsign in callsign_list) {
|
||||
console.log(callsign+' already in list.')
|
||||
return false
|
||||
}
|
||||
|
||||
var callsignTabs = $("#callsignTabs");
|
||||
tab_name = tab_string(callsign);
|
||||
tab_content = tab_content_name(callsign);
|
||||
divname = content_divname(callsign);
|
||||
|
||||
item_html = '<div class="active item" id="'+tab_name+'" onclick="openCallsign(event, \''+callsign+'\');">'+callsign+'</div>';
|
||||
callsignTabs.append(item_html);
|
||||
|
||||
callsign_list[callsign] = {'name': callsign, 'value': callsign, 'text': callsign}
|
||||
return true
|
||||
}
|
||||
|
||||
function append_message(callsign, msg, msg_html) {
|
||||
console.log('append_message');
|
||||
new_callsign = false
|
||||
if (!message_list.hasOwnProperty(callsign)) {
|
||||
message_list[callsign] = new Array();
|
||||
}
|
||||
message_list[callsign].push(msg);
|
||||
|
||||
// Find the right div to place the html
|
||||
new_callsign = add_callsign(callsign);
|
||||
append_message_html(callsign, msg_html, new_callsign);
|
||||
if (new_callsign) {
|
||||
//click on the new tab
|
||||
click_div = '#'+tab_string(callsign);
|
||||
console.log("Click on "+click_div);
|
||||
$(click_div).click();
|
||||
}
|
||||
}
|
||||
|
||||
function tab_string(callsign) {
|
||||
return "msgs"+callsign;
|
||||
}
|
||||
|
||||
function tab_content_name(callsign) {
|
||||
return tab_string(callsign)+"Content";
|
||||
}
|
||||
|
||||
function content_divname(callsign) {
|
||||
return "#"+tab_content_name(callsign);
|
||||
}
|
||||
|
||||
function append_message_html(callsign, msg_html, new_callsign) {
|
||||
var msgsTabs = $('#msgsTabsDiv');
|
||||
divname_str = tab_content_name(callsign);
|
||||
divname = content_divname(callsign);
|
||||
if (new_callsign) {
|
||||
// we have to add a new DIV
|
||||
msg_div_html = '<div class="tabcontent" id="'+divname_str+'" style="height:450px;">'+msg_html+'</div>';
|
||||
msgsTabs.append(msg_div_html);
|
||||
} else {
|
||||
var msgDiv = $(divname);
|
||||
msgDiv.append(msg_html);
|
||||
}
|
||||
|
||||
$(divname).animate({scrollTop: $(divname)[0].scrollHeight}, "slow");
|
||||
}
|
||||
|
||||
function create_message_html(time, from, to, message, ack) {
|
||||
msg_html = '<div class="item">';
|
||||
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>';
|
||||
} else {
|
||||
msg_html += '<i class="phone volume icon" data-content="Recieved Message"></i>';
|
||||
}
|
||||
msg_html += '<div class="middle aligned content">> </div>';
|
||||
msg_html += '</div>';
|
||||
msg_html += '<div class="middle aligned content">'+message+'</div>';
|
||||
msg_html += '</div><br>';
|
||||
|
||||
return msg_html
|
||||
}
|
||||
|
||||
function sent_msg(msg) {
|
||||
var msgsdiv = $("#sendMsgsDiv");
|
||||
|
||||
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);
|
||||
append_message(msg['to'], msg, msg_html);
|
||||
}
|
||||
|
||||
function from_msg(msg) {
|
||||
var msgsdiv = $("#sendMsgsDiv");
|
||||
|
||||
// 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
|
||||
|
||||
var d = new Date(ts).toLocaleDateString("en-US")
|
||||
var t = new Date(ts).toLocaleTimeString("en-US")
|
||||
|
||||
from = msg['from']
|
||||
msg_html = create_message_html(t, from, false, msg['message'], false);
|
||||
append_message(from, msg, msg_html);
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
|
||||
|
||||
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');
|
||||
}
|
||||
|
||||
function callsign_select(callsign) {
|
||||
var tocall = $("#to_call");
|
||||
tocall.val(callsign);
|
||||
}
|
||||
|
||||
function reset_Tabs() {
|
||||
tabcontent = document.getElementsByClassName("tabcontent");
|
||||
for (i = 0; i < tabcontent.length; i++) {
|
||||
tabcontent[i].style.display = "none";
|
||||
}
|
||||
}
|
||||
|
||||
function openCallsign(evt, callsign) {
|
||||
var i, tabcontent, tablinks;
|
||||
|
||||
tab_content = tab_content_name(callsign);
|
||||
|
||||
tabcontent = document.getElementsByClassName("tabcontent");
|
||||
for (i = 0; i < tabcontent.length; i++) {
|
||||
tabcontent[i].style.display = "none";
|
||||
}
|
||||
tablinks = document.getElementsByClassName("tablinks");
|
||||
for (i = 0; i < tablinks.length; i++) {
|
||||
tablinks[i].className = tablinks[i].className.replace(" active", "");
|
||||
}
|
||||
document.getElementById(tab_content).style.display = "block";
|
||||
evt.target.className += " active";
|
||||
callsign_select(callsign);
|
||||
}
|
||||
@@ -1,10 +1,26 @@
|
||||
var cleared = false;
|
||||
var callsign_list = {};
|
||||
var message_list = {};
|
||||
var from_msg_list = {};
|
||||
var selected_tab_callsign = null;
|
||||
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 raise_error(msg) {
|
||||
$.toast({
|
||||
heading: 'Error',
|
||||
text: msg,
|
||||
loader: true,
|
||||
loaderBg: '#9EC600',
|
||||
position: 'top-center',
|
||||
});
|
||||
}
|
||||
|
||||
function init_chat() {
|
||||
socket.on('connect', function () {
|
||||
console.log("Connected to socketio");
|
||||
@@ -15,202 +31,474 @@ function init_chat() {
|
||||
});
|
||||
|
||||
socket.on("sent", function(msg) {
|
||||
if (cleared == false) {
|
||||
console.log("SENT: ");
|
||||
console.log(msg);
|
||||
if (cleared === false) {
|
||||
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);
|
||||
});
|
||||
|
||||
$("#sendform").submit(function(event) {
|
||||
event.preventDefault();
|
||||
msg = {'to': $('#to_call').val(),
|
||||
'message': $('#message').val(),
|
||||
}
|
||||
socket.emit("send", msg);
|
||||
$('#message').val('');
|
||||
to_call = $('#to_call').val();
|
||||
message = $('#message').val();
|
||||
path = $('#pkt_path option:selected').val();
|
||||
if (to_call == "") {
|
||||
raise_error("You must enter a callsign to send a message")
|
||||
return false;
|
||||
} else {
|
||||
if (message == "") {
|
||||
raise_error("You must enter a message to send")
|
||||
return false;
|
||||
}
|
||||
msg = {'to': to_call, 'message': message, 'path': path};
|
||||
console.log(msg);
|
||||
socket.emit("send", msg);
|
||||
$('#message').val('');
|
||||
}
|
||||
});
|
||||
|
||||
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 */
|
||||
|
||||
if (callsign in callsign_list) {
|
||||
return false
|
||||
}
|
||||
|
||||
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>';
|
||||
callsignTabs.append(item_html);
|
||||
callsign_list[callsign] = true;
|
||||
return true
|
||||
function tab_string(callsign, id=false) {
|
||||
name = "msgs"+callsign;
|
||||
if (id) {
|
||||
return "#"+name;
|
||||
} else {
|
||||
return name;
|
||||
}
|
||||
}
|
||||
|
||||
function append_message(callsign, msg, msg_html) {
|
||||
new_callsign = false
|
||||
if (!message_list.hasOwnProperty(callsign)) {
|
||||
message_list[callsign] = new Array();
|
||||
}
|
||||
message_list[callsign].push(msg);
|
||||
|
||||
// Find the right div to place the html
|
||||
new_callsign = add_callsign(callsign);
|
||||
append_message_html(callsign, msg_html, new_callsign);
|
||||
if (new_callsign) {
|
||||
//click on the new tab
|
||||
click_div = '#'+tab_string(callsign);
|
||||
$(click_div).click();
|
||||
}
|
||||
function tab_li_string(callsign, id=false) {
|
||||
//The id of the LI containing the tab
|
||||
return tab_string(callsign,id)+"Li";
|
||||
}
|
||||
|
||||
function tab_string(callsign) {
|
||||
return "msgs"+callsign;
|
||||
function tab_notification_id(callsign, id=false) {
|
||||
// The ID of the span that contains the notification count
|
||||
return tab_string(callsign, id)+"notify";
|
||||
}
|
||||
|
||||
function tab_content_name(callsign) {
|
||||
return tab_string(callsign)+"Content";
|
||||
function tab_content_name(callsign, id=false) {
|
||||
return tab_string(callsign, id)+"Content";
|
||||
}
|
||||
|
||||
function tab_content_speech_wrapper(callsign, id=false) {
|
||||
return tab_string(callsign, id)+"SpeechWrapper";
|
||||
}
|
||||
|
||||
function tab_content_speech_wrapper_id(callsign) {
|
||||
return "#"+tab_content_speech_wrapper(callsign);
|
||||
}
|
||||
|
||||
function content_divname(callsign) {
|
||||
return "#"+tab_content_name(callsign);
|
||||
}
|
||||
|
||||
function append_message_html(callsign, msg_html, new_callsign) {
|
||||
var msgsTabs = $('#msgsTabsDiv');
|
||||
divname_str = tab_content_name(callsign);
|
||||
divname = content_divname(callsign);
|
||||
if (new_callsign) {
|
||||
// we have to add a new DIV
|
||||
msg_div_html = '<div class="tabcontent" id="'+divname_str+'" style="height:450px;">'+msg_html+'</div>';
|
||||
msgsTabs.append(msg_div_html);
|
||||
} else {
|
||||
var msgDiv = $(divname);
|
||||
msgDiv.append(msg_html);
|
||||
}
|
||||
|
||||
$(divname).animate({scrollTop: $(divname)[0].scrollHeight}, "slow");
|
||||
function callsign_tab(callsign) {
|
||||
return "#"+tab_string(callsign);
|
||||
}
|
||||
|
||||
function create_message_html(time, from, to, message, ack) {
|
||||
msg_html = '<div class="item">';
|
||||
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>';
|
||||
function bubble_msg_id(msg, id=false) {
|
||||
// The id of the div that contains a specific message
|
||||
name = msg["from_call"] + "_" + msg["msgNo"];
|
||||
if (id) {
|
||||
return "#"+name;
|
||||
} else {
|
||||
msg_html += '<i class="phone volume icon" data-content="Recieved Message"></i>';
|
||||
return name;
|
||||
}
|
||||
msg_html += '<div class="middle aligned content">> </div>';
|
||||
msg_html += '</div>';
|
||||
msg_html += '<div class="middle aligned content">'+message+'</div>';
|
||||
msg_html += '</div><br>';
|
||||
|
||||
return msg_html
|
||||
}
|
||||
|
||||
function sent_msg(msg) {
|
||||
var msgsdiv = $("#sendMsgsDiv");
|
||||
|
||||
ts_str = msg["ts"].toString();
|
||||
function message_ts_id(msg) {
|
||||
//Create a 'id' from the message timestamp
|
||||
ts_str = msg["timestamp"].toString();
|
||||
ts = ts_str.split(".")[0]*1000;
|
||||
id = ts_str.split('.')[0]
|
||||
id = ts_str.split('.')[0];
|
||||
return {'timestamp': ts, 'id': id};
|
||||
}
|
||||
|
||||
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};
|
||||
}
|
||||
|
||||
msg_html = create_message_html(t, msg['from'], msg['to'], msg['message'], ack_id);
|
||||
append_message(msg['to'], msg, msg_html);
|
||||
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'));
|
||||
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) {
|
||||
if (first_callsign === null) {
|
||||
first_callsign = callsign;
|
||||
active = true;
|
||||
} else {
|
||||
active = false;
|
||||
}
|
||||
create_callsign_tab(callsign, active);
|
||||
}
|
||||
// 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'];
|
||||
d = info['date'];
|
||||
ack_id = false;
|
||||
acked = false;
|
||||
if (msg['type'] == MSG_TYPE_TX) {
|
||||
ack_id = info['ack_id'];
|
||||
acked = msg['ack'];
|
||||
}
|
||||
msg_html = create_message_html(d, t, msg['from_call'], msg['to_call'],
|
||||
msg['message_text'], ack_id, msg, acked);
|
||||
append_message_html(callsign, msg_html, new_callsign);
|
||||
new_callsign = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (first_callsign !== null) {
|
||||
callsign_select(first_callsign);
|
||||
}
|
||||
}
|
||||
|
||||
function scroll_main_content(callsign=false) {
|
||||
var wc = $('#wc-content');
|
||||
var d = $('#msgsTabContent');
|
||||
var scrollHeight = wc.prop('scrollHeight');
|
||||
var clientHeight = wc.prop('clientHeight');
|
||||
|
||||
if (callsign) {
|
||||
div_id = content_divname(callsign);
|
||||
c_div = $(content_divname(callsign));
|
||||
//console.log("c_div("+div_id+") " + c_div);
|
||||
c_height = c_div.height();
|
||||
c_scroll_height = c_div.prop('scrollHeight');
|
||||
//console.log("callsign height " + c_height + " scrollHeight " + c_scroll_height);
|
||||
if (c_height === undefined) {
|
||||
return false;
|
||||
}
|
||||
if (c_height > clientHeight) {
|
||||
wc.animate({ scrollTop: c_scroll_height }, 500);
|
||||
} else {
|
||||
wc.animate({ scrollTop: 0 }, 500);
|
||||
}
|
||||
} else {
|
||||
if (scrollHeight > clientHeight) {
|
||||
wc.animate({ scrollTop: wc.prop('scrollHeight') }, 500);
|
||||
} else {
|
||||
wc.animate({ scrollTop: 0 }, 500);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function create_callsign_tab(callsign, active=false) {
|
||||
//Create the html for the callsign tab and insert it into the DOM
|
||||
var callsignTabs = $("#msgsTabList");
|
||||
tab_id = tab_string(callsign);
|
||||
tab_id_li = tab_li_string(callsign);
|
||||
tab_notify_id = tab_notification_id(callsign);
|
||||
tab_content = tab_content_name(callsign);
|
||||
if (active) {
|
||||
active_str = "active";
|
||||
} else {
|
||||
active_str = "";
|
||||
}
|
||||
|
||||
item_html = '<li class="nav-item" role="presentation" callsign="'+callsign+'" id="'+tab_id_li+'">';
|
||||
//item_html += '<button onClick="callsign_select(\''+callsign+'\');" callsign="'+callsign+'" class="nav-link '+active_str+'" id="'+tab_id+'" data-bs-toggle="tab" data-bs-target="#'+tab_content+'" type="button" role="tab" aria-controls="'+callsign+'" aria-selected="true">';
|
||||
item_html += '<button onClick="callsign_select(\''+callsign+'\');" callsign="'+callsign+'" class="nav-link position-relative '+active_str+'" id="'+tab_id+'" data-bs-toggle="tab" data-bs-target="#'+tab_content+'" type="button" role="tab" aria-controls="'+callsign+'" aria-selected="true">';
|
||||
item_html += callsign+' ';
|
||||
item_html += '<span id="'+tab_notify_id+'" class="position-absolute top-0 start-80 translate-middle badge bg-danger border border-light rounded-pill visually-hidden">0</span>';
|
||||
item_html += '<span onclick="delete_tab(\''+callsign+'\');">×</span>';
|
||||
item_html += '</button></li>'
|
||||
callsignTabs.append(item_html);
|
||||
create_callsign_tab_content(callsign, active);
|
||||
}
|
||||
|
||||
function create_callsign_tab_content(callsign, active=false) {
|
||||
var callsignTabsContent = $("#msgsTabContent");
|
||||
tab_id = tab_string(callsign);
|
||||
tab_content = tab_content_name(callsign);
|
||||
wrapper_id = tab_content_speech_wrapper(callsign);
|
||||
if (active) {
|
||||
active_str = "show active";
|
||||
} else {
|
||||
active_str = '';
|
||||
}
|
||||
|
||||
item_html = '<div class="tab-pane fade '+active_str+'" id="'+tab_content+'" role="tabpanel" aria-labelledby="'+tab_id+'">';
|
||||
item_html += '<div class="speech-wrapper" id="'+wrapper_id+'"></div>';
|
||||
item_html += '</div>';
|
||||
callsignTabsContent.append(item_html);
|
||||
}
|
||||
|
||||
function delete_tab(callsign) {
|
||||
// User asked to delete the tab and the conversation
|
||||
tab_id = tab_string(callsign, true);
|
||||
tab_id_li = tab_li_string(callsign, true);
|
||||
tab_content = tab_content_name(callsign, true);
|
||||
$(tab_id_li).remove();
|
||||
$(tab_content).remove();
|
||||
delete callsign_list[callsign];
|
||||
delete message_list[callsign];
|
||||
|
||||
// Now select the first tab
|
||||
first_tab = $("#msgsTabList").children().first().children().first();
|
||||
console.log(first_tab);
|
||||
$(first_tab).click();
|
||||
save_data();
|
||||
}
|
||||
|
||||
function add_callsign(callsign, msg) {
|
||||
/* Ensure a callsign exists in the left hand nav */
|
||||
if (callsign in callsign_list) {
|
||||
return false
|
||||
}
|
||||
len = Object.keys(callsign_list).length;
|
||||
if (len == 0) {
|
||||
active = true;
|
||||
} else {
|
||||
active = false;
|
||||
}
|
||||
create_callsign_tab(callsign, active);
|
||||
callsign_list[callsign] = '';
|
||||
return true;
|
||||
}
|
||||
|
||||
function update_callsign_path(callsign, path) {
|
||||
//Get the selected path to save for this callsign
|
||||
path = msg['path']
|
||||
console.log("Path is " + path);
|
||||
$('#pkt_path').val(path);
|
||||
callsign_list[callsign] = path;
|
||||
|
||||
}
|
||||
|
||||
function append_message(callsign, msg, msg_html) {
|
||||
new_callsign = false
|
||||
if (!message_list.hasOwnProperty(callsign)) {
|
||||
//message_list[callsign] = new Array();
|
||||
message_list[callsign] = {};
|
||||
}
|
||||
ts_id = message_ts_id(msg);
|
||||
id = ts_id['id']
|
||||
message_list[callsign][id] = msg;
|
||||
if (selected_tab_callsign != callsign) {
|
||||
// We need to update the notification for the tab
|
||||
tab_notify_id = tab_notification_id(callsign, true);
|
||||
// get the current count of notifications
|
||||
count = parseInt($(tab_notify_id).text());
|
||||
count += 1;
|
||||
$(tab_notify_id).text(count);
|
||||
$(tab_notify_id).removeClass('visually-hidden');
|
||||
}
|
||||
|
||||
// Find the right div to place the html
|
||||
|
||||
new_callsign = add_callsign(callsign, msg);
|
||||
update_callsign_path(callsign, msg['path']);
|
||||
append_message_html(callsign, msg_html, new_callsign);
|
||||
if (new_callsign) {
|
||||
//Now click the tab
|
||||
callsign_tab_id = callsign_tab(callsign);
|
||||
$(callsign_tab_id).click();
|
||||
callsign_select(callsign);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function append_message_html(callsign, msg_html, new_callsign) {
|
||||
var msgsTabs = $('#msgsTabsDiv');
|
||||
divname_str = tab_content_name(callsign);
|
||||
divname = content_divname(callsign);
|
||||
tab_content = tab_content_name(callsign);
|
||||
wrapper_id = tab_content_speech_wrapper_id(callsign);
|
||||
|
||||
$(wrapper_id).append(msg_html);
|
||||
|
||||
if ($(wrapper_id).children().length > 0) {
|
||||
$(wrapper_id).animate({scrollTop: $(wrapper_id)[0].scrollHeight}, "fast");
|
||||
}
|
||||
}
|
||||
|
||||
function create_message_html(date, time, from, to, message, ack_id, msg, acked=false) {
|
||||
div_id = from + "_" + msg.msgNo;
|
||||
if (ack_id) {
|
||||
alt = " alt"
|
||||
} else {
|
||||
alt = ""
|
||||
}
|
||||
|
||||
bubble_class = "bubble" + alt + " text-nowrap"
|
||||
bubble_name_class = "bubble-name" + alt
|
||||
bubble_msgid = bubble_msg_id(msg);
|
||||
date_str = date + " " + time;
|
||||
sane_date_str = date_str.replace(/ /g,"").replaceAll("/","").replaceAll(":","");
|
||||
|
||||
msg_html = '<div class="bubble-row'+alt+'">';
|
||||
msg_html += '<div id="'+bubble_msgid+'" class="'+ bubble_class + '" data-bs-toggle="popover" data-bs-content="'+msg['raw']+'">';
|
||||
msg_html += '<div class="bubble-text">';
|
||||
msg_html += '<p class="'+ bubble_name_class +'">'+from+' ';
|
||||
msg_html += '<span class="bubble-timestamp">'+date_str+'</span>';
|
||||
if (ack_id) {
|
||||
if (acked) {
|
||||
msg_html += '<span class="material-symbols-rounded md-10" id="' + ack_id + '">thumb_up</span>';
|
||||
} else {
|
||||
msg_html += '<span class="material-symbols-rounded md-10" id="' + ack_id + '">thumb_down</span>';
|
||||
}
|
||||
}
|
||||
msg_html += "</p>";
|
||||
bubble_msg_class = "bubble-message"
|
||||
if (ack_id) {
|
||||
bubble_arrow_class = "bubble-arrow alt"
|
||||
popover_placement = "left"
|
||||
} else {
|
||||
bubble_arrow_class = "bubble-arrow"
|
||||
popover_placement = "right"
|
||||
}
|
||||
|
||||
msg_html += '<p class="' +bubble_msg_class+ '">'+message+'</p>';
|
||||
msg_html += '<div class="'+ bubble_arrow_class + '"></div>';
|
||||
msg_html += "</div></div></div>";
|
||||
|
||||
popover_html = '\n<script>$(function () {$(\'[data-bs-toggle="popover"]\').popover('
|
||||
popover_html += '{title: "APRS Raw Packet", html: false, trigger: \'hover\', placement: \''+popover_placement+'\'});})';
|
||||
popover_html += '</script>'
|
||||
|
||||
return msg_html+popover_html
|
||||
}
|
||||
|
||||
function flash_message(msg) {
|
||||
// Callback function to bring a hidden box back
|
||||
msg_id = bubble_msg_id(msg, true);
|
||||
$(msg_id).fadeOut(100).fadeIn(100).fadeOut(100).fadeIn(100).fadeOut(100).fadeIn(100);
|
||||
}
|
||||
|
||||
function sent_msg(msg) {
|
||||
info = time_ack_from_msg(msg);
|
||||
t = info['time'];
|
||||
d = info['date'];
|
||||
ack_id = info['ack_id'];
|
||||
|
||||
msg_html = create_message_html(d, t, msg['from_call'], msg['to_call'], msg['message_text'], ack_id, msg, false);
|
||||
append_message(msg['to_call'], msg, msg_html);
|
||||
save_data();
|
||||
scroll_main_content(msg['from_call']);
|
||||
}
|
||||
|
||||
function from_msg(msg) {
|
||||
var msgsdiv = $("#sendMsgsDiv");
|
||||
if (!from_msg_list.hasOwnProperty(msg["from_call"])) {
|
||||
from_msg_list[msg["from_call"]] = 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["msgNo"] in from_msg_list[msg["from_call"]]) {
|
||||
// We already have this message
|
||||
console.log("We already have this message msgNo=" + msg["msgNo"]);
|
||||
// Do some flashy thing?
|
||||
flash_message(msg);
|
||||
return false
|
||||
} else {
|
||||
console.log("Adding message " + msg["msgNo"] + " to " + msg["from_call"]);
|
||||
from_msg_list[msg["from_call"]][msg["msgNo"]] = msg
|
||||
}
|
||||
info = time_ack_from_msg(msg);
|
||||
t = info['time'];
|
||||
d = info['date'];
|
||||
ack_id = info['ack_id'];
|
||||
|
||||
var d = new Date(ts).toLocaleDateString("en-US")
|
||||
var t = new Date(ts).toLocaleTimeString("en-US")
|
||||
|
||||
from = msg['from']
|
||||
msg_html = create_message_html(t, from, false, msg['message'], false);
|
||||
from = msg['from_call']
|
||||
msg_html = create_message_html(d, t, from, false, msg['message_text'], false, msg, false);
|
||||
append_message(from, msg, msg_html);
|
||||
save_data();
|
||||
scroll_main_content(from);
|
||||
}
|
||||
|
||||
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
|
||||
// We have an existing entry
|
||||
ts_id = message_ts_id(msg);
|
||||
id = ts_id['id'];
|
||||
//Mark the message as acked
|
||||
callsign = msg['to_call'];
|
||||
// Ensure the message_list has this callsign
|
||||
if (!message_list.hasOwnProperty(callsign)) {
|
||||
return false
|
||||
}
|
||||
// Ensure the message_list has this id
|
||||
if (!message_list[callsign].hasOwnProperty(id)) {
|
||||
return false
|
||||
}
|
||||
if (message_list[callsign][id]['ack'] == true) {
|
||||
return false;
|
||||
}
|
||||
message_list[callsign][id]['ack'] = true;
|
||||
ack_id = "ack_" + id
|
||||
|
||||
if (msg['ack'] == true) {
|
||||
var ack_div = $('#' + ack_id);
|
||||
ack_div.html('thumb_up');
|
||||
}
|
||||
|
||||
|
||||
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();
|
||||
scroll_main_content();
|
||||
}
|
||||
|
||||
function callsign_select(callsign) {
|
||||
var tocall = $("#to_call");
|
||||
tocall.val(callsign);
|
||||
}
|
||||
|
||||
function reset_Tabs() {
|
||||
tabcontent = document.getElementsByClassName("tabcontent");
|
||||
for (i = 0; i < tabcontent.length; i++) {
|
||||
tabcontent[i].style.display = "none";
|
||||
}
|
||||
}
|
||||
|
||||
function openCallsign(evt, callsign) {
|
||||
var i, tabcontent, tablinks;
|
||||
|
||||
tab_content = tab_content_name(callsign);
|
||||
|
||||
tabcontent = document.getElementsByClassName("tabcontent");
|
||||
for (i = 0; i < tabcontent.length; i++) {
|
||||
tabcontent[i].style.display = "none";
|
||||
}
|
||||
tablinks = document.getElementsByClassName("tablinks");
|
||||
for (i = 0; i < tablinks.length; i++) {
|
||||
tablinks[i].className = tablinks[i].className.replace(" active", "");
|
||||
}
|
||||
document.getElementById(tab_content).style.display = "block";
|
||||
evt.target.className += " active";
|
||||
callsign_select(callsign);
|
||||
var tocall = $("#to_call");
|
||||
tocall.val(callsign);
|
||||
scroll_main_content(callsign);
|
||||
selected_tab_callsign = callsign;
|
||||
tab_notify_id = tab_notification_id(callsign, true);
|
||||
$(tab_notify_id).addClass('visually-hidden');
|
||||
$(tab_notify_id).text(0);
|
||||
// Now update the path
|
||||
$('#pkt_path').val(callsign_list[callsign]);
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,374 @@
|
||||
// jQuery toast plugin created by Kamran Ahmed copyright MIT license 2015
|
||||
if ( typeof Object.create !== 'function' ) {
|
||||
Object.create = function( obj ) {
|
||||
function F() {}
|
||||
F.prototype = obj;
|
||||
return new F();
|
||||
};
|
||||
}
|
||||
|
||||
(function( $, window, document, undefined ) {
|
||||
|
||||
"use strict";
|
||||
|
||||
var Toast = {
|
||||
|
||||
_positionClasses : ['bottom-left', 'bottom-right', 'top-right', 'top-left', 'bottom-center', 'top-center', 'mid-center'],
|
||||
_defaultIcons : ['success', 'error', 'info', 'warning'],
|
||||
|
||||
init: function (options, elem) {
|
||||
this.prepareOptions(options, $.toast.options);
|
||||
this.process();
|
||||
},
|
||||
|
||||
prepareOptions: function(options, options_to_extend) {
|
||||
var _options = {};
|
||||
if ( ( typeof options === 'string' ) || ( options instanceof Array ) ) {
|
||||
_options.text = options;
|
||||
} else {
|
||||
_options = options;
|
||||
}
|
||||
this.options = $.extend( {}, options_to_extend, _options );
|
||||
},
|
||||
|
||||
process: function () {
|
||||
this.setup();
|
||||
this.addToDom();
|
||||
this.position();
|
||||
this.bindToast();
|
||||
this.animate();
|
||||
},
|
||||
|
||||
setup: function () {
|
||||
|
||||
var _toastContent = '';
|
||||
|
||||
this._toastEl = this._toastEl || $('<div></div>', {
|
||||
class : 'jq-toast-single'
|
||||
});
|
||||
|
||||
// For the loader on top
|
||||
_toastContent += '<span class="jq-toast-loader"></span>';
|
||||
|
||||
if ( this.options.allowToastClose ) {
|
||||
_toastContent += '<span class="close-jq-toast-single">×</span>';
|
||||
};
|
||||
|
||||
if ( this.options.text instanceof Array ) {
|
||||
|
||||
if ( this.options.heading ) {
|
||||
_toastContent +='<h2 class="jq-toast-heading">' + this.options.heading + '</h2>';
|
||||
};
|
||||
|
||||
_toastContent += '<ul class="jq-toast-ul">';
|
||||
for (var i = 0; i < this.options.text.length; i++) {
|
||||
_toastContent += '<li class="jq-toast-li" id="jq-toast-item-' + i + '">' + this.options.text[i] + '</li>';
|
||||
}
|
||||
_toastContent += '</ul>';
|
||||
|
||||
} else {
|
||||
if ( this.options.heading ) {
|
||||
_toastContent +='<h2 class="jq-toast-heading">' + this.options.heading + '</h2>';
|
||||
};
|
||||
_toastContent += this.options.text;
|
||||
}
|
||||
|
||||
this._toastEl.html( _toastContent );
|
||||
|
||||
if ( this.options.bgColor !== false ) {
|
||||
this._toastEl.css("background-color", this.options.bgColor);
|
||||
};
|
||||
|
||||
if ( this.options.textColor !== false ) {
|
||||
this._toastEl.css("color", this.options.textColor);
|
||||
};
|
||||
|
||||
if ( this.options.textAlign ) {
|
||||
this._toastEl.css('text-align', this.options.textAlign);
|
||||
}
|
||||
|
||||
if ( this.options.icon !== false ) {
|
||||
this._toastEl.addClass('jq-has-icon');
|
||||
|
||||
if ( $.inArray(this.options.icon, this._defaultIcons) !== -1 ) {
|
||||
this._toastEl.addClass('jq-icon-' + this.options.icon);
|
||||
};
|
||||
};
|
||||
|
||||
if ( this.options.class !== false ){
|
||||
this._toastEl.addClass(this.options.class)
|
||||
}
|
||||
},
|
||||
|
||||
position: function () {
|
||||
if ( ( typeof this.options.position === 'string' ) && ( $.inArray( this.options.position, this._positionClasses) !== -1 ) ) {
|
||||
|
||||
if ( this.options.position === 'bottom-center' ) {
|
||||
this._container.css({
|
||||
left: ( $(window).outerWidth() / 2 ) - this._container.outerWidth()/2,
|
||||
bottom: 20
|
||||
});
|
||||
} else if ( this.options.position === 'top-center' ) {
|
||||
this._container.css({
|
||||
left: ( $(window).outerWidth() / 2 ) - this._container.outerWidth()/2,
|
||||
top: 20
|
||||
});
|
||||
} else if ( this.options.position === 'mid-center' ) {
|
||||
this._container.css({
|
||||
left: ( $(window).outerWidth() / 2 ) - this._container.outerWidth()/2,
|
||||
top: ( $(window).outerHeight() / 2 ) - this._container.outerHeight()/2
|
||||
});
|
||||
} else {
|
||||
this._container.addClass( this.options.position );
|
||||
}
|
||||
|
||||
} else if ( typeof this.options.position === 'object' ) {
|
||||
this._container.css({
|
||||
top : this.options.position.top ? this.options.position.top : 'auto',
|
||||
bottom : this.options.position.bottom ? this.options.position.bottom : 'auto',
|
||||
left : this.options.position.left ? this.options.position.left : 'auto',
|
||||
right : this.options.position.right ? this.options.position.right : 'auto'
|
||||
});
|
||||
} else {
|
||||
this._container.addClass( 'bottom-left' );
|
||||
}
|
||||
},
|
||||
|
||||
bindToast: function () {
|
||||
|
||||
var that = this;
|
||||
|
||||
this._toastEl.on('afterShown', function () {
|
||||
that.processLoader();
|
||||
});
|
||||
|
||||
this._toastEl.find('.close-jq-toast-single').on('click', function ( e ) {
|
||||
|
||||
e.preventDefault();
|
||||
|
||||
if( that.options.showHideTransition === 'fade') {
|
||||
that._toastEl.trigger('beforeHide');
|
||||
that._toastEl.fadeOut(function () {
|
||||
that._toastEl.trigger('afterHidden');
|
||||
});
|
||||
} else if ( that.options.showHideTransition === 'slide' ) {
|
||||
that._toastEl.trigger('beforeHide');
|
||||
that._toastEl.slideUp(function () {
|
||||
that._toastEl.trigger('afterHidden');
|
||||
});
|
||||
} else {
|
||||
that._toastEl.trigger('beforeHide');
|
||||
that._toastEl.hide(function () {
|
||||
that._toastEl.trigger('afterHidden');
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
if ( typeof this.options.beforeShow == 'function' ) {
|
||||
this._toastEl.on('beforeShow', function () {
|
||||
that.options.beforeShow(that._toastEl);
|
||||
});
|
||||
};
|
||||
|
||||
if ( typeof this.options.afterShown == 'function' ) {
|
||||
this._toastEl.on('afterShown', function () {
|
||||
that.options.afterShown(that._toastEl);
|
||||
});
|
||||
};
|
||||
|
||||
if ( typeof this.options.beforeHide == 'function' ) {
|
||||
this._toastEl.on('beforeHide', function () {
|
||||
that.options.beforeHide(that._toastEl);
|
||||
});
|
||||
};
|
||||
|
||||
if ( typeof this.options.afterHidden == 'function' ) {
|
||||
this._toastEl.on('afterHidden', function () {
|
||||
that.options.afterHidden(that._toastEl);
|
||||
});
|
||||
};
|
||||
|
||||
if ( typeof this.options.onClick == 'function' ) {
|
||||
this._toastEl.on('click', function () {
|
||||
that.options.onClick(that._toastEl);
|
||||
});
|
||||
};
|
||||
},
|
||||
|
||||
addToDom: function () {
|
||||
|
||||
var _container = $('.jq-toast-wrap');
|
||||
|
||||
if ( _container.length === 0 ) {
|
||||
|
||||
_container = $('<div></div>',{
|
||||
class: "jq-toast-wrap",
|
||||
role: "alert",
|
||||
"aria-live": "polite"
|
||||
});
|
||||
|
||||
$('body').append( _container );
|
||||
|
||||
} else if ( !this.options.stack || isNaN( parseInt(this.options.stack, 10) ) ) {
|
||||
_container.empty();
|
||||
}
|
||||
|
||||
_container.find('.jq-toast-single:hidden').remove();
|
||||
|
||||
_container.append( this._toastEl );
|
||||
|
||||
if ( this.options.stack && !isNaN( parseInt( this.options.stack ), 10 ) ) {
|
||||
|
||||
var _prevToastCount = _container.find('.jq-toast-single').length,
|
||||
_extToastCount = _prevToastCount - this.options.stack;
|
||||
|
||||
if ( _extToastCount > 0 ) {
|
||||
$('.jq-toast-wrap').find('.jq-toast-single').slice(0, _extToastCount).remove();
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
this._container = _container;
|
||||
},
|
||||
|
||||
canAutoHide: function () {
|
||||
return ( this.options.hideAfter !== false ) && !isNaN( parseInt( this.options.hideAfter, 10 ) );
|
||||
},
|
||||
|
||||
processLoader: function () {
|
||||
// Show the loader only, if auto-hide is on and loader is demanded
|
||||
if (!this.canAutoHide() || this.options.loader === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
var loader = this._toastEl.find('.jq-toast-loader');
|
||||
|
||||
// 400 is the default time that jquery uses for fade/slide
|
||||
// Divide by 1000 for milliseconds to seconds conversion
|
||||
var transitionTime = (this.options.hideAfter - 400) / 1000 + 's';
|
||||
var loaderBg = this.options.loaderBg;
|
||||
|
||||
var style = loader.attr('style') || '';
|
||||
style = style.substring(0, style.indexOf('-webkit-transition')); // Remove the last transition definition
|
||||
|
||||
style += '-webkit-transition: width ' + transitionTime + ' ease-in; \
|
||||
-o-transition: width ' + transitionTime + ' ease-in; \
|
||||
transition: width ' + transitionTime + ' ease-in; \
|
||||
background-color: ' + loaderBg + ';';
|
||||
|
||||
|
||||
loader.attr('style', style).addClass('jq-toast-loaded');
|
||||
},
|
||||
|
||||
animate: function () {
|
||||
|
||||
var that = this;
|
||||
|
||||
this._toastEl.hide();
|
||||
|
||||
this._toastEl.trigger('beforeShow');
|
||||
|
||||
if ( this.options.showHideTransition.toLowerCase() === 'fade' ) {
|
||||
this._toastEl.fadeIn(function ( ){
|
||||
that._toastEl.trigger('afterShown');
|
||||
});
|
||||
} else if ( this.options.showHideTransition.toLowerCase() === 'slide' ) {
|
||||
this._toastEl.slideDown(function ( ){
|
||||
that._toastEl.trigger('afterShown');
|
||||
});
|
||||
} else {
|
||||
this._toastEl.show(function ( ){
|
||||
that._toastEl.trigger('afterShown');
|
||||
});
|
||||
}
|
||||
|
||||
if (this.canAutoHide()) {
|
||||
|
||||
var that = this;
|
||||
|
||||
window.setTimeout(function(){
|
||||
|
||||
if ( that.options.showHideTransition.toLowerCase() === 'fade' ) {
|
||||
that._toastEl.trigger('beforeHide');
|
||||
that._toastEl.fadeOut(function () {
|
||||
that._toastEl.trigger('afterHidden');
|
||||
});
|
||||
} else if ( that.options.showHideTransition.toLowerCase() === 'slide' ) {
|
||||
that._toastEl.trigger('beforeHide');
|
||||
that._toastEl.slideUp(function () {
|
||||
that._toastEl.trigger('afterHidden');
|
||||
});
|
||||
} else {
|
||||
that._toastEl.trigger('beforeHide');
|
||||
that._toastEl.hide(function () {
|
||||
that._toastEl.trigger('afterHidden');
|
||||
});
|
||||
}
|
||||
|
||||
}, this.options.hideAfter);
|
||||
};
|
||||
},
|
||||
|
||||
reset: function ( resetWhat ) {
|
||||
|
||||
if ( resetWhat === 'all' ) {
|
||||
$('.jq-toast-wrap').remove();
|
||||
} else {
|
||||
this._toastEl.remove();
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
update: function(options) {
|
||||
this.prepareOptions(options, this.options);
|
||||
this.setup();
|
||||
this.bindToast();
|
||||
},
|
||||
|
||||
close: function() {
|
||||
this._toastEl.find('.close-jq-toast-single').click();
|
||||
}
|
||||
};
|
||||
|
||||
$.toast = function(options) {
|
||||
var toast = Object.create(Toast);
|
||||
toast.init(options, this);
|
||||
|
||||
return {
|
||||
|
||||
reset: function ( what ) {
|
||||
toast.reset( what );
|
||||
},
|
||||
|
||||
update: function( options ) {
|
||||
toast.update( options );
|
||||
},
|
||||
|
||||
close: function( ) {
|
||||
toast.close( );
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
$.toast.options = {
|
||||
text: '',
|
||||
heading: '',
|
||||
showHideTransition: 'fade',
|
||||
allowToastClose: true,
|
||||
hideAfter: 3000,
|
||||
loader: true,
|
||||
loaderBg: '#9EC600',
|
||||
stack: 5,
|
||||
position: 'bottom-left',
|
||||
bgColor: false,
|
||||
textColor: false,
|
||||
textAlign: 'left',
|
||||
icon: false,
|
||||
beforeShow: function () {},
|
||||
afterShown: function () {},
|
||||
beforeHide: function () {},
|
||||
afterHidden: function () {},
|
||||
onClick: function () {}
|
||||
};
|
||||
|
||||
})( jQuery, window, document );
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,85 +1,136 @@
|
||||
<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>
|
||||
<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.7.1/jquery.min.js"></script>
|
||||
<script src="/static/js/upstream/jquery.toast.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.13.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 href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.1/dist/css/bootstrap.min.css"
|
||||
rel="stylesheet"
|
||||
integrity="sha384-4bw+/aepP/YC94hEpVNVgiZdgIC5+VKNBQNGCHeKRQN+PtmoHDEXuppvnDJzQIu9"
|
||||
crossorigin="anonymous">
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.1/dist/js/bootstrap.bundle.min.js"
|
||||
integrity="sha384-HwwvtgBNo3bZJJLYd8oVXjrBZt8cqVSpeBNS5n7C8IVInixGAoxmnlMuBnhbgrkm"
|
||||
crossorigin="anonymous"></script>
|
||||
|
||||
<link rel="stylesheet" href="/static/css/index.css">
|
||||
<link rel="stylesheet" href="/static/css/tabs.css">
|
||||
<script src="/static/js/main.js"></script>
|
||||
<script src="/static/js/gps.js"></script>
|
||||
<script src="/static/js/send-message.js"></script>
|
||||
<link rel="stylesheet"
|
||||
href="https://fonts.googleapis.com/css2?family=Material+Symbols+Rounded:opsz,wght,FILL,GRAD@24,200,1,200">
|
||||
<link rel="stylesheet" href="/static/css/upstream/jquery.toast.css">
|
||||
|
||||
<script type="text/javascript">
|
||||
var initial_stats = {{ initial_stats|tojson|safe }};
|
||||
<link rel="stylesheet" href="/static/css/chat.css">
|
||||
<link rel="stylesheet" href="/static/css/index.css">
|
||||
<link rel="stylesheet" href="/static/css/tabs.css">
|
||||
<script src="/static/js/main.js"></script>
|
||||
<script src="/static/js/gps.js"></script>
|
||||
<script src="/static/js/send-message.js"></script>
|
||||
|
||||
var memory_chart = null
|
||||
var message_chart = null
|
||||
<script type="text/javascript">
|
||||
var initial_stats = {{ initial_stats|tojson|safe }};
|
||||
var latitude = parseFloat('{{ latitude|safe }}');
|
||||
var longitude = parseFloat('{{ longitude|safe }}');
|
||||
|
||||
$(document).ready(function() {
|
||||
console.log(initial_stats);
|
||||
start_update();
|
||||
init_chat();
|
||||
reset_Tabs();
|
||||
var memory_chart = null;
|
||||
var message_chart = null;
|
||||
|
||||
$(document).ready(function() {
|
||||
console.log(initial_stats);
|
||||
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>
|
||||
|
||||
<body>
|
||||
<div class='ui text container'>
|
||||
<h1 class='ui dividing header'>APRSD WebChat {{ version }}</h1>
|
||||
</div>
|
||||
// When a tab is clicked, populate the to_call form field.
|
||||
$(document).on('shown.bs.tab', 'button[data-bs-toggle="tab"]', function (e) {
|
||||
var tab = $(e.target);
|
||||
var callsign = tab.attr("callsign");
|
||||
var to_call = $('#to_call');
|
||||
to_call.val(callsign);
|
||||
selected_tab_callsign = callsign;
|
||||
});
|
||||
});
|
||||
|
||||
<div class='ui grid text container'>
|
||||
<div class='left floated ten wide column'>
|
||||
<span style='color: green'>{{ callsign }}</span>
|
||||
connected to
|
||||
<span style='color: blue' id='aprs_connection'>{{ aprs_connection|safe }}</span>
|
||||
</div>
|
||||
</script>
|
||||
</head>
|
||||
|
||||
<div class='right floated four wide column'>
|
||||
<span id='uptime'>NONE</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="ui container">
|
||||
<h3 class="ui dividing header">Send Message</h3>
|
||||
<div id="sendMsgDiv" class="ui mini text">
|
||||
<form id="sendform" name="sendmsg" action="">
|
||||
<div class="ui corner labeled input">
|
||||
<label for="to_call" class="ui label">Callsign</label>
|
||||
<input type="text" name="to_call" id="to_call" placeholder="To Callsign" size="11" maxlength="9">
|
||||
<div class="ui corner label">
|
||||
<i class="asterisk icon"></i>
|
||||
<body>
|
||||
<div class="wc-container">
|
||||
<div class="wc-row header">
|
||||
<div class="container-sm">
|
||||
<div class="row">
|
||||
<div class="column">
|
||||
<h1>APRSD WebChat {{ version }}</h1>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="column">
|
||||
<span style='color: green'>{{ callsign }}</span>
|
||||
connected to
|
||||
<span style='color: blue' id='aprs_connection'>{{ aprs_connection|safe }}</span>
|
||||
<span id='uptime'>NONE</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<form class="row gx-1 gy-1 justify-content-center align-items-center" id="sendform" name="sendmsg" action="" autocomplete="off">
|
||||
<div class="col-sm-2" style="width:150px;">
|
||||
<label for="to_call" class="visually-hidden">Callsign</label>
|
||||
<input type="search" class="form-control mb-2 mr-sm-2" name="to_call" id="to_call" placeholder="To Callsign" size="11" maxlength="9">
|
||||
</div>
|
||||
</div>
|
||||
<div class="ui labeled input">
|
||||
<label for="message" class="ui label">Message</label>
|
||||
<input type="text" name="message" id="message" size="40" maxlength="40">
|
||||
</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>
|
||||
</form>
|
||||
<div class="col-auto">
|
||||
<label for="pkt_path" class="visually-hidden">PATH</label>
|
||||
<select class="form-control mb-2 mr-sm-2" name="pkt_path" id="pkt_path" style="width:auto;">
|
||||
<option value="" selected>Default Path</option>
|
||||
<option value="WIDE1-1">WIDE1-1</option>
|
||||
<option value="WIDE1-1,WIDE2-1">WIDE1-1,WIDE2-1</option>
|
||||
<option value="ARISS">ARISS</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-sm-3">
|
||||
<label for="message" class="visually-hidden">Message</label>
|
||||
<input type="search" class="form-control mb-2 mr-sm-2" name="message" id="message" size="40" maxlength="67" placeholder="Message">
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<input type="submit" name="submit" class="btn btn-primary mb-2" id="send_msg" value="Send"/>
|
||||
<button type="button" class="btn btn-primary mb-2" id="send_beacon" value="Send Position">Send Position</button>
|
||||
<!-- <button type="button" class="btn btn-primary mb-2" id="wipe_local" value="wipe local storage">Wipe LocalStorage</button> -->
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="container-sm" style="max-width: 800px">
|
||||
<ul class="nav nav-tabs" id="msgsTabList" role="tablist"></ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="ui grid">
|
||||
<div class="three wide column">
|
||||
<div class="tab" id="callsignTabs"></div>
|
||||
</div>
|
||||
<div class="ten wide column ui raised segment" id="msgsTabsDiv" style="height:450px;padding:0px;">
|
||||
|
||||
<div class="wc-row content" id="wc-content">
|
||||
<div class="container" style="max-width: 800px;">
|
||||
<div class="tab-content" id="msgsTabContent">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="ui text container" style="padding-top: 40px">
|
||||
<a href="https://badge.fury.io/py/aprsd"><img src="https://badge.fury.io/py/aprsd.svg" alt="PyPI version" height="18"></a>
|
||||
<a href="https://github.com/craigerl/aprsd"><img src="https://img.shields.io/badge/Made%20with-Python-1f425f.svg" height="18"></a>
|
||||
<div class="wc-row footer">
|
||||
<div class="container-sm" style="padding-top: 40px">
|
||||
<a href="https://badge.fury.io/py/aprsd"><img src="https://badge.fury.io/py/aprsd.svg" alt="PyPI version" height="18"></a>
|
||||
<a href="https://github.com/craigerl/aprsd"><img src="https://img.shields.io/badge/Made%20with-Python-1f425f.svg" height="18"></a>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,82 +0,0 @@
|
||||
<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>
|
||||
|
||||
<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="/static/css/index.css">
|
||||
<script src="/static/js/main.js"></script>
|
||||
<script src="/static/js/gps.js"></script>
|
||||
<script src="/static/js/send-message-mobile.js"></script>
|
||||
|
||||
<script type="text/javascript">
|
||||
var initial_stats = {{ initial_stats|tojson|safe }};
|
||||
|
||||
var memory_chart = null
|
||||
var message_chart = null
|
||||
|
||||
$(document).ready(function() {
|
||||
console.log(initial_stats);
|
||||
start_update();
|
||||
init_chat();
|
||||
});
|
||||
</script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class='ui text container'>
|
||||
<h1 class='ui dividing header'>APRSD WebChat {{ version }}</h1>
|
||||
</div>
|
||||
|
||||
<div class='ui grid text container' style="padding-bottom: 5px;">
|
||||
<div class='left floated twelve wide column'>
|
||||
<span style='color: green'>{{ callsign }}</span>
|
||||
connected to
|
||||
<span style='color: blue' id='aprs_connection'>{{ aprs_connection|safe }}</span>
|
||||
</div>
|
||||
|
||||
<div class='right floated four wide column'>
|
||||
<span id='uptime'>NONE</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="sendMsgDiv" class="ui grid" align="left" style="padding-top: 2px;">
|
||||
<h3 class="sixteen wide column ui dividing header">Send Message</h3>
|
||||
<form id="sendform" name="sendmsg" action="">
|
||||
<div class="sixteen wide column ui left labeled icon input">
|
||||
<div class="ui label">Callsign</div>
|
||||
<input type="text" name="to_call" id="to_call" placeholder="To Callsign" size="11" maxlength="9">
|
||||
<i class="users icon"></i>
|
||||
</div>
|
||||
<div class="sixteen wide column ui left labeled icon input" style="padding-bottom: 5px;">
|
||||
<label for="message" class="ui label">Message</label>
|
||||
<input type="text" name="message" id="message" maxlength="40" placeholder="Message">
|
||||
<i class="comment icon"></i>
|
||||
</div>
|
||||
<div class="right floated column">
|
||||
<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>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="ui grid">
|
||||
<div class="ui top attached tabular raised menu" id="callsignTabs">
|
||||
</div>
|
||||
<div class="sixteen wide column ui bottom attached raised tab segment" id="msgsTabsDiv" style="height:250px;padding:5px;">
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="ui text container" style="padding-top: 40px">
|
||||
<a href="https://badge.fury.io/py/aprsd"><img src="https://badge.fury.io/py/aprsd.svg" alt="PyPI version" height="18"></a>
|
||||
<a href="https://github.com/craigerl/aprsd"><img src="https://img.shields.io/badge/Made%20with-Python-1f425f.svg" height="18"></a>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
+6
-1
@@ -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":
|
||||
|
||||
+204
-79
@@ -1,87 +1,212 @@
|
||||
#
|
||||
# This file is autogenerated by pip-compile with Python 3.10
|
||||
# This file is autogenerated by pip-compile with Python 3.11
|
||||
# by the following command:
|
||||
#
|
||||
# pip-compile --annotation-style=line dev-requirements.in
|
||||
#
|
||||
add-trailing-comma==3.0.0 # via gray
|
||||
alabaster==0.7.13 # via sphinx
|
||||
attrs==23.1.0 # via jsonschema, referencing
|
||||
autoflake==1.5.3 # via gray
|
||||
babel==2.12.1 # via sphinx
|
||||
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
|
||||
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
|
||||
distlib==0.3.7 # via virtualenv
|
||||
docutils==0.20.1 # via sphinx
|
||||
exceptiongroup==1.1.2 # 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
|
||||
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
|
||||
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-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-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
|
||||
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
|
||||
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
|
||||
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
|
||||
pyyaml==6.0.1 # via fixit, libcst, pre-commit
|
||||
referencing==0.30.0 # 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
|
||||
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
|
||||
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
|
||||
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
|
||||
add-trailing-comma==3.1.0
|
||||
# via gray
|
||||
alabaster==0.7.13
|
||||
# via sphinx
|
||||
attrs==23.1.0
|
||||
# via
|
||||
# jsonschema
|
||||
# referencing
|
||||
autoflake==1.5.3
|
||||
# via gray
|
||||
babel==2.12.1
|
||||
# via sphinx
|
||||
black==23.7.0
|
||||
# via gray
|
||||
build==1.0.3
|
||||
# via pip-tools
|
||||
cachetools==5.3.1
|
||||
# via tox
|
||||
certifi==2023.7.22
|
||||
# via requests
|
||||
cfgv==3.4.0
|
||||
# via pre-commit
|
||||
chardet==5.2.0
|
||||
# via tox
|
||||
charset-normalizer==3.2.0
|
||||
# via requests
|
||||
click==8.1.7
|
||||
# via
|
||||
# black
|
||||
# pip-tools
|
||||
colorama==0.4.6
|
||||
# via tox
|
||||
commonmark==0.9.1
|
||||
# via rich
|
||||
configargparse==1.7
|
||||
# via gray
|
||||
coverage[toml]==7.3.1
|
||||
# via pytest-cov
|
||||
distlib==0.3.7
|
||||
# via virtualenv
|
||||
docutils==0.20.1
|
||||
# via sphinx
|
||||
filelock==3.12.3
|
||||
# via
|
||||
# tox
|
||||
# virtualenv
|
||||
fixit==0.1.4
|
||||
# via gray
|
||||
flake8==6.1.0
|
||||
# via
|
||||
# -r dev-requirements.in
|
||||
# fixit
|
||||
# pep8-naming
|
||||
gray==0.13.0
|
||||
# via -r dev-requirements.in
|
||||
identify==2.5.27
|
||||
# via pre-commit
|
||||
idna==3.4
|
||||
# via requests
|
||||
imagesize==1.4.1
|
||||
# via sphinx
|
||||
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.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.5.1
|
||||
# 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.2
|
||||
# via black
|
||||
pep8-naming==0.13.3
|
||||
# via -r dev-requirements.in
|
||||
pip-tools==7.3.0
|
||||
# via -r dev-requirements.in
|
||||
platformdirs==3.10.0
|
||||
# via
|
||||
# black
|
||||
# tox
|
||||
# virtualenv
|
||||
pluggy==1.3.0
|
||||
# via
|
||||
# pytest
|
||||
# tox
|
||||
pre-commit==3.4.0
|
||||
# via -r dev-requirements.in
|
||||
pycodestyle==2.11.0
|
||||
# via flake8
|
||||
pyflakes==3.1.0
|
||||
# via
|
||||
# autoflake
|
||||
# flake8
|
||||
pygments==2.16.1
|
||||
# via
|
||||
# rich
|
||||
# sphinx
|
||||
pyproject-api==1.6.1
|
||||
# via tox
|
||||
pyproject-hooks==1.0.0
|
||||
# via build
|
||||
pytest==7.4.2
|
||||
# via
|
||||
# -r dev-requirements.in
|
||||
# pytest-cov
|
||||
pytest-cov==4.1.0
|
||||
# via -r dev-requirements.in
|
||||
pyupgrade==3.10.1
|
||||
# via gray
|
||||
pyyaml==6.0.1
|
||||
# via
|
||||
# fixit
|
||||
# libcst
|
||||
# pre-commit
|
||||
referencing==0.30.2
|
||||
# via
|
||||
# jsonschema
|
||||
# jsonschema-specifications
|
||||
requests==2.31.0
|
||||
# via sphinx
|
||||
rich==12.6.0
|
||||
# via gray
|
||||
rpds-py==0.10.2
|
||||
# via
|
||||
# jsonschema
|
||||
# referencing
|
||||
snowballstemmer==2.2.0
|
||||
# via sphinx
|
||||
sphinx==7.2.5
|
||||
# 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.6
|
||||
# via sphinx
|
||||
sphinxcontrib-serializinghtml==1.1.9
|
||||
# via sphinx
|
||||
tokenize-rt==5.2.0
|
||||
# via
|
||||
# add-trailing-comma
|
||||
# pyupgrade
|
||||
toml==0.10.2
|
||||
# via autoflake
|
||||
tox==4.11.2
|
||||
# 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.6
|
||||
# via requests
|
||||
virtualenv==20.24.5
|
||||
# via
|
||||
# pre-commit
|
||||
# tox
|
||||
wheel==0.41.2
|
||||
# via pip-tools
|
||||
|
||||
# The following packages are considered to be unsafe in a requirements file:
|
||||
# pip
|
||||
|
||||
+46
-47
@@ -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
|
||||
|
||||
HEALTHCHECK --interval=5m --timeout=12s --start-period=30s \
|
||||
CMD aprsd healthcheck --config /config/aprsd.conf
|
||||
ENTRYPOINT ["/app/run.sh"]
|
||||
VOLUME ["/config"]
|
||||
|
||||
# Set the user to run the application
|
||||
USER appuser
|
||||
|
||||
HEALTHCHECK --interval=1m --timeout=12s --start-period=30s \
|
||||
CMD aprsd healthcheck --config /config/aprsd.conf --loglevel DEBUG
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
-----------------------------
|
||||
|
||||
|
||||
@@ -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
@@ -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
|
||||
---------------
|
||||
|
||||
|
||||
@@ -27,16 +27,12 @@ wrapt
|
||||
# kiss3 uses attrs
|
||||
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
|
||||
|
||||
+172
-77
@@ -1,85 +1,180 @@
|
||||
#
|
||||
# This file is autogenerated by pip-compile with Python 3.10
|
||||
# This file is autogenerated by pip-compile with Python 3.11
|
||||
# by the following command:
|
||||
#
|
||||
# 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
|
||||
blinker==1.6.2 # via flask
|
||||
certifi==2023.7.22 # via httpcore, requests
|
||||
cffi==1.15.1 # via cryptography
|
||||
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
|
||||
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
|
||||
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
|
||||
imapclient==2.3.1 # via -r requirements.in
|
||||
importlib-metadata==6.8.0 # via ax253, kiss3
|
||||
itsdangerous==2.1.2 # via flask
|
||||
jinja2==3.1.2 # via click-completion, flask
|
||||
kiss3==8.0.0 # via -r requirements.in
|
||||
markupsafe==2.1.3 # via jinja2, werkzeug
|
||||
netaddr==0.8.0 # via oslo-config
|
||||
oslo-config==9.1.1 # via -r requirements.in
|
||||
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
|
||||
pyserial==3.5 # via pyserial-asyncio
|
||||
pyserial-asyncio==0.6 # via kiss3
|
||||
python-engineio==4.5.1 # via python-socketio
|
||||
python-socketio==5.8.0 # via -r requirements.in, flask-socketio
|
||||
pytz==2023.3 # via -r requirements.in
|
||||
pyyaml==6.0.1 # via -r requirements.in, oslo-config
|
||||
requests==2.31.0 # via -r requirements.in, oslo-config, update-checker
|
||||
rfc3986==2.0.0 # via oslo-config
|
||||
rich==12.6.0 # via -r requirements.in
|
||||
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
|
||||
thesmuggler==1.0.1 # via -r requirements.in
|
||||
ua-parser==0.18.0 # via user-agents
|
||||
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
|
||||
wrapt==1.15.0 # via -r requirements.in, debtcollector
|
||||
zipp==3.16.2 # via importlib-metadata
|
||||
zope-event==5.0 # via gevent
|
||||
zope-interface==6.0 # via gevent
|
||||
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.1
|
||||
# via
|
||||
# ax253
|
||||
# kiss3
|
||||
blinker==1.6.2
|
||||
# via flask
|
||||
certifi==2023.7.22
|
||||
# via requests
|
||||
charset-normalizer==3.2.0
|
||||
# via requests
|
||||
click==8.1.7
|
||||
# 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
|
||||
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.2
|
||||
# via eventlet
|
||||
eventlet==0.33.3
|
||||
# via -r requirements.in
|
||||
flask==2.3.3
|
||||
# via
|
||||
# -r requirements.in
|
||||
# flask-httpauth
|
||||
# flask-socketio
|
||||
flask-httpauth==4.8.0
|
||||
# via -r requirements.in
|
||||
flask-socketio==5.3.6
|
||||
# via -r requirements.in
|
||||
geographiclib==2.0
|
||||
# via geopy
|
||||
geopy==2.4.0
|
||||
# via -r requirements.in
|
||||
gevent==23.9.1
|
||||
# via -r requirements.in
|
||||
greenlet==3.0.0rc3
|
||||
# via
|
||||
# eventlet
|
||||
# gevent
|
||||
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
|
||||
jinja2==3.1.2
|
||||
# via
|
||||
# click-completion
|
||||
# flask
|
||||
kiss3==8.0.0
|
||||
# via -r requirements.in
|
||||
markupsafe==2.1.3
|
||||
# via
|
||||
# jinja2
|
||||
# werkzeug
|
||||
netaddr==0.8.0
|
||||
# via oslo-config
|
||||
oslo-config==9.2.0
|
||||
# via -r requirements.in
|
||||
oslo-i18n==6.1.0
|
||||
# via oslo-config
|
||||
pbr==5.11.1
|
||||
# via
|
||||
# -r requirements.in
|
||||
# oslo-i18n
|
||||
# stevedore
|
||||
pluggy==1.3.0
|
||||
# via -r requirements.in
|
||||
plumbum==1.8.2
|
||||
# via rpyc
|
||||
pygments==2.16.1
|
||||
# via rich
|
||||
pyserial==3.5
|
||||
# via pyserial-asyncio
|
||||
pyserial-asyncio==0.6
|
||||
# via kiss3
|
||||
python-engineio==4.7.0
|
||||
# via python-socketio
|
||||
python-socketio==5.9.0
|
||||
# via
|
||||
# -r requirements.in
|
||||
# flask-socketio
|
||||
pytz==2023.3.post1
|
||||
# via -r requirements.in
|
||||
pyyaml==6.0.1
|
||||
# via
|
||||
# -r requirements.in
|
||||
# oslo-config
|
||||
requests==2.31.0
|
||||
# via
|
||||
# -r requirements.in
|
||||
# oslo-config
|
||||
# update-checker
|
||||
rfc3986==2.0.0
|
||||
# via oslo-config
|
||||
rich==12.6.0
|
||||
# via -r requirements.in
|
||||
rpyc==5.3.1
|
||||
# via -r requirements.in
|
||||
rush==2021.4.0
|
||||
# via -r requirements.in
|
||||
shellingham==1.5.3
|
||||
# via
|
||||
# -r requirements.in
|
||||
# click-completion
|
||||
six==1.16.0
|
||||
# via
|
||||
# -r requirements.in
|
||||
# click-completion
|
||||
# eventlet
|
||||
# imapclient
|
||||
soupsieve==2.5
|
||||
# via beautifulsoup4
|
||||
stevedore==5.1.0
|
||||
# via oslo-config
|
||||
tabulate==0.9.0
|
||||
# via -r requirements.in
|
||||
thesmuggler==1.0.1
|
||||
# via -r requirements.in
|
||||
update-checker==0.18.0
|
||||
# via -r requirements.in
|
||||
urllib3==2.0.6
|
||||
# via requests
|
||||
validators==0.20.0
|
||||
# via click-params
|
||||
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
|
||||
zope-interface==6.0
|
||||
# via gevent
|
||||
|
||||
# The following packages are considered to be unsafe in a requirements file:
|
||||
# setuptools
|
||||
|
||||
Reference in New Issue
Block a user