1
0
mirror of https://github.com/craigerl/aprsd.git synced 2025-04-19 09:49:01 -04:00

Remove webchat as a built in command.

Webchat will now be an extension that can be installed.
the extension is here:
https://github.com/hemna/aprsd-webchat-extension

Install it from git or pypi.
This commit is contained in:
Hemna 2024-12-09 16:53:51 -05:00
parent c48ff8dfd4
commit 8f8887f0e4
63 changed files with 11 additions and 5172 deletions

View File

@ -1,643 +0,0 @@
import datetime
import json
import logging
import signal
import sys
import threading
import time
import click
import flask
from flask import request
from flask_httpauth import HTTPBasicAuth
from flask_socketio import Namespace, SocketIO
from geopy.distance import geodesic
from oslo_config import cfg
import timeago
import wrapt
import aprsd
from aprsd import cli_helper, client, packets, plugin_utils, stats, threads
from aprsd import utils
from aprsd import utils as aprsd_utils
from aprsd.client import client_factory, kiss
from aprsd.main import cli
from aprsd.threads import aprsd as aprsd_threads
from aprsd.threads import keep_alive, rx
from aprsd.threads import stats as stats_thread
from aprsd.threads import tx
from aprsd.utils import trace
CONF = cfg.CONF
LOG = logging.getLogger()
auth = HTTPBasicAuth()
socketio = None
# List of callsigns that we don't want to track/fetch their location
callsign_no_track = [
"APDW16", "BLN0", "BLN1", "BLN2",
"BLN3", "BLN4", "BLN5", "BLN6", "BLN7", "BLN8", "BLN9",
]
# Callsign location information
# callsign: {lat: 0.0, long: 0.0, last_update: datetime}
callsign_locations = {}
flask_app = flask.Flask(
"aprsd",
static_url_path="/static",
static_folder="web/chat/static",
template_folder="web/chat/templates",
)
def signal_handler(sig, frame):
click.echo("signal_handler: called")
LOG.info(
f"Ctrl+C, Sending all threads({len(threads.APRSDThreadList())}) exit! "
f"Can take up to 10 seconds {datetime.datetime.now()}",
)
threads.APRSDThreadList().stop_all()
if "subprocess" not in str(frame):
time.sleep(1.5)
stats.stats_collector.collect()
LOG.info("Telling flask to bail.")
signal.signal(signal.SIGTERM, sys.exit(0))
class SentMessages:
_instance = None
lock = threading.Lock()
data = {}
def __new__(cls, *args, **kwargs):
"""This magic turns this into a singleton."""
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance
def is_initialized(self):
return True
@wrapt.synchronized(lock)
def add(self, msg):
self.data[msg.msgNo] = msg.__dict__
@wrapt.synchronized(lock)
def __len__(self):
return len(self.data.keys())
@wrapt.synchronized(lock)
def get(self, id):
if id in self.data:
return self.data[id]
@wrapt.synchronized(lock)
def get_all(self):
return self.data
@wrapt.synchronized(lock)
def set_status(self, id, status):
if id in self.data:
self.data[id]["last_update"] = str(datetime.datetime.now())
self.data[id]["status"] = status
@wrapt.synchronized(lock)
def ack(self, id):
"""The message got an ack!"""
if id in self.data:
self.data[id]["last_update"] = str(datetime.datetime.now())
self.data[id]["ack"] = True
@wrapt.synchronized(lock)
def reply(self, id, packet):
"""We got a packet back from the sent message."""
if id in self.data:
self.data[id]["reply"] = packet
def _build_location_from_repeat(message):
# This is a location message Format is
# ^ld^callsign:latitude,longitude,altitude,course,speed,timestamp
a = message.split(":")
LOG.warning(a)
if len(a) == 2:
callsign = a[0].replace("^ld^", "")
b = a[1].split(",")
LOG.warning(b)
if len(b) == 6:
lat = float(b[0])
lon = float(b[1])
alt = float(b[2])
course = float(b[3])
speed = float(b[4])
time = int(b[5])
compass_bearing = aprsd_utils.degrees_to_cardinal(course)
data = {
"callsign": callsign,
"lat": lat,
"lon": lon,
"altitude": alt,
"course": course,
"compass_bearing": compass_bearing,
"speed": speed,
"lasttime": time,
"timeago": timeago.format(time),
}
LOG.debug(f"Location data from REPEAT {data}")
return data
def _calculate_location_data(location_data):
"""Calculate all of the location data from data from aprs.fi or REPEAT."""
lat = location_data["lat"]
lon = location_data["lon"]
alt = location_data["altitude"]
speed = location_data["speed"]
lasttime = location_data["lasttime"]
timeago_str = location_data.get(
"timeago",
timeago.format(lasttime),
)
# now calculate distance from our own location
distance = 0
if CONF.webchat.latitude and CONF.webchat.longitude:
our_lat = float(CONF.webchat.latitude)
our_lon = float(CONF.webchat.longitude)
distance = geodesic((our_lat, our_lon), (lat, lon)).kilometers
bearing = aprsd_utils.calculate_initial_compass_bearing(
(our_lat, our_lon),
(lat, lon),
)
compass_bearing = aprsd_utils.degrees_to_cardinal(bearing)
return {
"callsign": location_data["callsign"],
"lat": lat,
"lon": lon,
"altitude": alt,
"course": f"{bearing:0.1f}",
"compass_bearing": compass_bearing,
"speed": speed,
"lasttime": lasttime,
"timeago": timeago_str,
"distance": f"{distance:0.1f}",
}
def send_location_data_to_browser(location_data):
global socketio
callsign = location_data["callsign"]
LOG.info(f"Got location for {callsign} {callsign_locations[callsign]}")
socketio.emit(
"callsign_location", callsign_locations[callsign],
namespace="/sendmsg",
)
def populate_callsign_location(callsign, data=None):
"""Populate the location for the callsign.
if data is passed in, then we have the location already from
an APRS packet. If data is None, then we need to fetch the
location from aprs.fi or REPEAT.
"""
global socketio
"""Fetch the location for the callsign."""
LOG.debug(f"populate_callsign_location {callsign}")
if data:
location_data = _calculate_location_data(data)
callsign_locations[callsign] = location_data
send_location_data_to_browser(location_data)
return
# First we are going to try to get the location from aprs.fi
# if there is no internets, then this will fail and we will
# fallback to calling REPEAT for the location for the callsign.
fallback = False
if not CONF.aprs_fi.apiKey:
LOG.warning(
"Config aprs_fi.apiKey is not set. Can't get location from aprs.fi "
" falling back to sending REPEAT to get location.",
)
fallback = True
else:
try:
aprs_data = plugin_utils.get_aprs_fi(CONF.aprs_fi.apiKey, callsign)
if not len(aprs_data["entries"]):
LOG.error("Didn't get any entries from aprs.fi")
return
lat = float(aprs_data["entries"][0]["lat"])
lon = float(aprs_data["entries"][0]["lng"])
try: # altitude not always provided
alt = float(aprs_data["entries"][0]["altitude"])
except Exception:
alt = 0
location_data = {
"callsign": callsign,
"lat": lat,
"lon": lon,
"altitude": alt,
"lasttime": int(aprs_data["entries"][0]["lasttime"]),
"course": float(aprs_data["entries"][0].get("course", 0)),
"speed": float(aprs_data["entries"][0].get("speed", 0)),
}
location_data = _calculate_location_data(location_data)
callsign_locations[callsign] = location_data
send_location_data_to_browser(location_data)
return
except Exception as ex:
LOG.error(f"Failed to fetch aprs.fi '{ex}'")
LOG.error(ex)
fallback = True
if fallback:
# We don't have the location data
# and we can't get it from aprs.fi
# Send a special message to REPEAT to get the location data
LOG.info(f"Sending REPEAT to get location for callsign {callsign}.")
tx.send(
packets.MessagePacket(
from_call=CONF.callsign,
to_call="REPEAT",
message_text=f"ld {callsign}",
),
)
class WebChatProcessPacketThread(rx.APRSDProcessPacketThread):
"""Class that handles packets being sent to us."""
def __init__(self, packet_queue, socketio):
self.socketio = socketio
self.connected = False
super().__init__(packet_queue)
def process_ack_packet(self, packet: packets.AckPacket):
super().process_ack_packet(packet)
ack_num = packet.get("msgNo")
SentMessages().ack(ack_num)
msg = SentMessages().get(ack_num)
if msg:
self.socketio.emit(
"ack", msg,
namespace="/sendmsg",
)
self.got_ack = True
def process_our_message_packet(self, packet: packets.MessagePacket):
global callsign_locations
# ok lets see if we have the location for the
# person we just sent a message to.
from_call = packet.get("from_call").upper()
if from_call == "REPEAT":
# We got a message from REPEAT. Is this a location message?
message = packet.get("message_text")
if message.startswith("^ld^"):
location_data = _build_location_from_repeat(message)
callsign = location_data["callsign"]
location_data = _calculate_location_data(location_data)
callsign_locations[callsign] = location_data
send_location_data_to_browser(location_data)
return
elif (
from_call not in callsign_locations
and from_call not in callsign_no_track
and client_factory.create().transport() in [client.TRANSPORT_APRSIS, client.TRANSPORT_FAKE]
):
# We have to ask aprs for the location for the callsign
# We send a message packet to wb4bor-11 asking for location.
populate_callsign_location(from_call)
# Send the packet to the browser.
self.socketio.emit(
"new", packet.__dict__,
namespace="/sendmsg",
)
class LocationProcessingThread(aprsd_threads.APRSDThread):
"""Class to handle the location processing."""
def __init__(self):
super().__init__("LocationProcessingThread")
def loop(self):
pass
def _get_transport(stats):
if CONF.aprs_network.enabled:
transport = "aprs-is"
aprs_connection = (
"APRS-IS Server: <a href='http://status.aprs2.net' >"
"{}</a>".format(stats["APRSClientStats"]["server_string"])
)
elif kiss.KISSClient.is_enabled():
transport = kiss.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
@flask_app.route("/location/<callsign>", methods=["POST"])
def location(callsign):
LOG.debug(f"Fetch location for callsign {callsign}")
if not callsign in callsign_no_track:
populate_callsign_location(callsign)
@auth.login_required
@flask_app.route("/")
def index():
stats = _stats()
# For development
html_template = "index.html"
LOG.debug(f"Template {html_template}")
transport, aprs_connection = _get_transport(stats["stats"])
LOG.debug(f"transport {transport} aprs_connection {aprs_connection}")
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,
initial_stats=stats,
aprs_connection=aprs_connection,
callsign=CONF.callsign,
version=aprsd.__version__,
latitude=latitude,
longitude=longitude,
)
@auth.login_required
@flask_app.route("/send-message-status")
def send_message_status():
LOG.debug(request)
msgs = SentMessages()
info = msgs.get_all()
return json.dumps(info)
def _stats():
now = datetime.datetime.now()
time_format = "%m-%d-%Y %H:%M:%S"
stats_dict = stats.stats_collector.collect(serializable=True)
# Webchat doesnt need these
if "WatchList" in stats_dict:
del stats_dict["WatchList"]
if "SeenList" in stats_dict:
del stats_dict["SeenList"]
if "APRSDThreadList" in stats_dict:
del stats_dict["APRSDThreadList"]
if "PacketList" in stats_dict:
del stats_dict["PacketList"]
if "EmailStats" in stats_dict:
del stats_dict["EmailStats"]
if "PluginManager" in stats_dict:
del stats_dict["PluginManager"]
result = {
"time": now.strftime(time_format),
"stats": stats_dict,
}
return result
@flask_app.route("/stats")
def get_stats():
return json.dumps(_stats())
class SendMessageNamespace(Namespace):
"""Class to handle the socketio interactions."""
got_ack = False
reply_sent = False
msg = None
request = None
def __init__(self, namespace=None, config=None):
super().__init__(namespace)
def on_connect(self):
global socketio
LOG.debug("Web socket connected")
socketio.emit(
"connected", {"data": "/sendmsg Connected"},
namespace="/sendmsg",
)
def on_disconnect(self):
LOG.debug("WS Disconnected")
def on_send(self, data):
global socketio
LOG.debug(f"WS: on_send {data}")
self.request = data
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()
tx.send(pkt)
msgs.add(pkt)
msgs.set_status(pkt.msgNo, "Sending")
obj = msgs.get(pkt.msgNo)
socketio.emit(
"sent", obj,
namespace="/sendmsg",
)
def on_gps(self, data):
LOG.debug(f"WS on_GPS: {data}")
lat = data["latitude"]
long = data["longitude"]
LOG.debug(f"Lat {lat}")
LOG.debug(f"Long {long}")
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]
tx.send(
packets.BeaconPacket(
from_call=CONF.callsign,
to_call="APDW16",
latitude=lat,
longitude=long,
comment="APRSD WebChat Beacon",
path=path,
),
direct=True,
)
def handle_message(self, data):
LOG.debug(f"WS Data {data}")
def handle_json(self, data):
LOG.debug(f"WS json {data}")
def on_get_callsign_location(self, data):
LOG.debug(f"on_callsign_location {data}")
if data["callsign"] not in callsign_no_track:
populate_callsign_location(data["callsign"])
@trace.trace
def init_flask(loglevel, quiet):
global socketio, flask_app
socketio = SocketIO(
flask_app, logger=False, engineio_logger=False,
async_mode="threading",
)
socketio.on_namespace(
SendMessageNamespace(
"/sendmsg",
),
)
return socketio
# main() ###
@cli.command()
@cli_helper.add_options(cli_helper.common_options)
@click.option(
"-f",
"--flush",
"flush",
is_flag=True,
show_default=True,
default=False,
help="Flush out all old aged messages on disk.",
)
@click.option(
"-p",
"--port",
"port",
show_default=True,
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
def webchat(ctx, flush, port):
"""Web based HAM Radio chat program!"""
loglevel = ctx.obj["loglevel"]
quiet = ctx.obj["quiet"]
signal.signal(signal.SIGINT, signal_handler)
signal.signal(signal.SIGTERM, signal_handler)
level, msg = utils._check_version()
if level:
LOG.warning(msg)
else:
LOG.info(msg)
LOG.info(f"APRSD Started version: {aprsd.__version__}")
CONF.log_opt_values(logging.getLogger(), logging.DEBUG)
if not port:
port = CONF.webchat.web_port
# Initialize the client factory and create
# The correct client object ready for use
# Make sure we have 1 client transport enabled
if not client_factory.is_client_enabled():
LOG.error("No Clients are enabled in config.")
sys.exit(-1)
if not client_factory.is_client_configured():
LOG.error("APRS client is not properly configured in config file.")
sys.exit(-1)
# Creates the client object
LOG.info("Creating client connection")
aprs_client = client_factory.create()
LOG.info(aprs_client)
if not aprs_client.login_success:
# We failed to login, will just quit!
msg = f"Login Failure: {aprs_client.login_failure}"
LOG.error(msg)
print(msg)
sys.exit(-1)
keepalive = keep_alive.KeepAliveThread()
LOG.info("Start KeepAliveThread")
keepalive.start()
stats_store_thread = stats_thread.APRSDStatsStoreThread()
stats_store_thread.start()
socketio = init_flask(loglevel, quiet)
rx_thread = rx.APRSDPluginRXThread(
packet_queue=threads.packet_queue,
)
rx_thread.start()
process_thread = WebChatProcessPacketThread(
packet_queue=threads.packet_queue,
socketio=socketio,
)
process_thread.start()
LOG.info("Start socketio.run()")
socketio.run(
flask_app,
# 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.")

View File

@ -11,17 +11,12 @@ watch_list_group = cfg.OptGroup(
name="watch_list",
title="Watch List settings",
)
webchat_group = cfg.OptGroup(
name="webchat",
title="Settings specific to the webchat command",
)
registry_group = cfg.OptGroup(
name="aprs_registry",
title="APRS Registry settings",
)
aprsd_opts = [
cfg.StrOpt(
"callsign",
@ -194,34 +189,6 @@ enabled_plugins_opts = [
),
]
webchat_opts = [
cfg.StrOpt(
"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.",
),
cfg.BoolOpt(
"disable_url_request_logging",
default=False,
help="Disable the logging of url requests in the webchat command.",
),
]
registry_opts = [
cfg.BoolOpt(
"enabled",
@ -261,8 +228,6 @@ def register_opts(config):
config.register_opts(enabled_plugins_opts)
config.register_group(watch_list_group)
config.register_opts(watch_list_opts, group=watch_list_group)
config.register_group(webchat_group)
config.register_opts(webchat_opts, group=webchat_group)
config.register_group(registry_group)
config.register_opts(registry_opts, group=registry_group)
@ -271,6 +236,5 @@ def list_opts():
return {
"DEFAULT": (aprsd_opts + enabled_plugins_opts),
watch_list_group.name: watch_list_opts,
webchat_group.name: webchat_opts,
registry_group.name: registry_opts,
}

View File

@ -68,19 +68,9 @@ def setup_logging(loglevel=None, quiet=False):
"aprslib.parsing",
"aprslib.exceptions",
]
webserver_list = [
"werkzeug",
"werkzeug._internal",
"socketio",
"urllib3.connectionpool",
"chardet",
"chardet.charsetgroupprober",
"chardet.eucjpprober",
"chardet.mbcharsetprober",
]
# We don't really want to see the aprslib parsing debug output.
disable_list = imap_list + aprslib_list + webserver_list
disable_list = imap_list + aprslib_list
# remove every other logger's handlers
# and propagate to root logger
@ -91,12 +81,6 @@ def setup_logging(loglevel=None, quiet=False):
else:
logging.getLogger(name).propagate = True
if CONF.webchat.disable_url_request_logging:
for name in webserver_list:
logging.getLogger(name).handlers = []
logging.getLogger(name).propagate = True
logging.getLogger(name).setLevel(logging.ERROR)
handlers = [
{
"sink": sys.stdout,

View File

@ -55,7 +55,7 @@ def cli(ctx):
def load_commands():
from .cmds import ( # noqa
completion, dev, fetch_stats, healthcheck, list_plugins, listen,
send_message, server, webchat,
send_message, server,
)

View File

View File

@ -1,84 +0,0 @@
body {
background: #eeeeee;
margin: 2em;
text-align: center;
font-family: system-ui, sans-serif;
}
footer {
padding: 2em;
text-align: center;
height: 10vh;
}
.ui.segment {
background: #eeeeee;
}
#graphs {
display: grid;
width: 100%;
height: 300px;
grid-template-columns: 1fr 1fr;
}
#graphs_center {
display: block;
margin-top: 10px;
margin-bottom: 10px;
width: 100%;
height: 300px;
}
#left {
margin-right: 2px;
height: 300px;
}
#right {
height: 300px;
}
#center {
height: 300px;
}
#packetsChart, #messageChart, #emailChart, #memChart {
border: 1px solid #ccc;
background: #ddd;
}
#stats {
margin: auto;
width: 80%;
}
#jsonstats {
display: none;
}
#title {
font-size: 4em;
}
#version{
font-size: .5em;
}
#uptime, #aprsis {
font-size: 1em;
}
#callsign {
font-size: 1.4em;
color: #00F;
padding-top: 8px;
margin:10px;
}
#title_rx {
background-color: darkseagreen;
text-align: left;
}
#title_tx {
background-color: lightcoral;
text-align: left;
}
.aprsd_1 {
background-image: url(/static/images/aprs-symbols-16-0.png);
background-repeat: no-repeat;
background-position: -160px -48px;
width: 16px;
height: 16px;
}

View File

@ -1,4 +0,0 @@
/* PrismJS 1.29.0
https://prismjs.com/download.html#themes=prism-tomorrow&languages=markup+css+clike+javascript+json+json5+log&plugins=show-language+toolbar */
code[class*=language-],pre[class*=language-]{color:#ccc;background:0 0;font-family:Consolas,Monaco,'Andale Mono','Ubuntu Mono',monospace;font-size:1em;text-align:left;white-space:pre;word-spacing:normal;word-break:normal;word-wrap:normal;line-height:1.5;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-hyphens:none;-moz-hyphens:none;-ms-hyphens:none;hyphens:none}pre[class*=language-]{padding:1em;margin:.5em 0;overflow:auto}:not(pre)>code[class*=language-],pre[class*=language-]{background:#2d2d2d}:not(pre)>code[class*=language-]{padding:.1em;border-radius:.3em;white-space:normal}.token.block-comment,.token.cdata,.token.comment,.token.doctype,.token.prolog{color:#999}.token.punctuation{color:#ccc}.token.attr-name,.token.deleted,.token.namespace,.token.tag{color:#e2777a}.token.function-name{color:#6196cc}.token.boolean,.token.function,.token.number{color:#f08d49}.token.class-name,.token.constant,.token.property,.token.symbol{color:#f8c555}.token.atrule,.token.builtin,.token.important,.token.keyword,.token.selector{color:#cc99cd}.token.attr-value,.token.char,.token.regex,.token.string,.token.variable{color:#7ec699}.token.entity,.token.operator,.token.url{color:#67cdcc}.token.bold,.token.important{font-weight:700}.token.italic{font-style:italic}.token.entity{cursor:help}.token.inserted{color:green}
div.code-toolbar{position:relative}div.code-toolbar>.toolbar{position:absolute;z-index:10;top:.3em;right:.2em;transition:opacity .3s ease-in-out;opacity:0}div.code-toolbar:hover>.toolbar{opacity:1}div.code-toolbar:focus-within>.toolbar{opacity:1}div.code-toolbar>.toolbar>.toolbar-item{display:inline-block}div.code-toolbar>.toolbar>.toolbar-item>a{cursor:pointer}div.code-toolbar>.toolbar>.toolbar-item>button{background:0 0;border:0;color:inherit;font:inherit;line-height:normal;overflow:visible;padding:0;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none}div.code-toolbar>.toolbar>.toolbar-item>a,div.code-toolbar>.toolbar>.toolbar-item>button,div.code-toolbar>.toolbar>.toolbar-item>span{color:#bbb;font-size:.8em;padding:0 .5em;background:#f5f2f0;background:rgba(224,224,224,.2);box-shadow:0 2px 0 0 rgba(0,0,0,.2);border-radius:.5em}div.code-toolbar>.toolbar>.toolbar-item>a:focus,div.code-toolbar>.toolbar>.toolbar-item>a:hover,div.code-toolbar>.toolbar>.toolbar-item>button:focus,div.code-toolbar>.toolbar>.toolbar-item>button:hover,div.code-toolbar>.toolbar>.toolbar-item>span:focus,div.code-toolbar>.toolbar>.toolbar-item>span:hover{color:inherit;text-decoration:none}

View File

@ -1,35 +0,0 @@
/* Style the tab */
.tab {
overflow: hidden;
border: 1px solid #ccc;
background-color: #f1f1f1;
}
/* Style the buttons that are used to open the tab content */
.tab button {
background-color: inherit;
float: left;
border: none;
outline: none;
cursor: pointer;
padding: 14px 16px;
transition: 0.3s;
}
/* Change background color of buttons on hover */
.tab button:hover {
background-color: #ddd;
}
/* Create an active/current tablink class */
.tab button.active {
background-color: #ccc;
}
/* Style the tab content */
.tabcontent {
display: none;
padding: 6px 12px;
border: 1px solid #ccc;
border-top: none;
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

View File

@ -1,235 +0,0 @@
var packet_list = {};
window.chartColors = {
red: 'rgb(255, 99, 132)',
orange: 'rgb(255, 159, 64)',
yellow: 'rgb(255, 205, 86)',
green: 'rgb(26, 181, 77)',
blue: 'rgb(54, 162, 235)',
purple: 'rgb(153, 102, 255)',
grey: 'rgb(201, 203, 207)',
black: 'rgb(0, 0, 0)',
lightcoral: 'rgb(240,128,128)',
darkseagreen: 'rgb(143, 188,143)'
};
function size_dict(d){c=0; for (i in d) ++c; return c}
function start_charts() {
Chart.scaleService.updateScaleDefaults('linear', {
ticks: {
min: 0
}
});
packets_chart = new Chart($("#packetsChart"), {
label: 'APRS Packets',
type: 'line',
data: {
labels: [],
datasets: [{
label: 'Packets Sent',
borderColor: window.chartColors.lightcoral,
data: [],
},
{
label: 'Packets Recieved',
borderColor: window.chartColors.darkseagreen,
data: [],
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
title: {
display: true,
text: 'APRS Packets',
},
scales: {
x: {
type: 'timeseries',
offset: true,
ticks: {
major: { enabled: true },
fontStyle: context => context.tick.major ? 'bold' : undefined,
source: 'data',
maxRotation: 0,
autoSkip: true,
autoSkipPadding: 75,
}
}
}
}
});
message_chart = new Chart($("#messageChart"), {
label: 'Messages',
type: 'line',
data: {
labels: [],
datasets: [{
label: 'Messages Sent',
borderColor: window.chartColors.lightcoral,
data: [],
},
{
label: 'Messages Recieved',
borderColor: window.chartColors.darkseagreen,
data: [],
},
{
label: 'Ack Sent',
borderColor: window.chartColors.purple,
data: [],
},
{
label: 'Ack Recieved',
borderColor: window.chartColors.black,
data: [],
}],
},
options: {
responsive: true,
maintainAspectRatio: false,
title: {
display: true,
text: 'APRS Messages',
},
scales: {
x: {
type: 'timeseries',
offset: true,
ticks: {
major: { enabled: true },
fontStyle: context => context.tick.major ? 'bold' : undefined,
source: 'data',
maxRotation: 0,
autoSkip: true,
autoSkipPadding: 75,
}
}
}
}
});
email_chart = new Chart($("#emailChart"), {
label: 'Email Messages',
type: 'line',
data: {
labels: [],
datasets: [{
label: 'Sent',
borderColor: window.chartColors.lightcoral,
data: [],
},
{
label: 'Recieved',
borderColor: window.chartColors.darkseagreen,
data: [],
}],
},
options: {
responsive: true,
maintainAspectRatio: false,
title: {
display: true,
text: 'Email Messages',
},
scales: {
x: {
type: 'timeseries',
offset: true,
ticks: {
major: { enabled: true },
fontStyle: context => context.tick.major ? 'bold' : undefined,
source: 'data',
maxRotation: 0,
autoSkip: true,
autoSkipPadding: 75,
}
}
}
}
});
memory_chart = new Chart($("#memChart"), {
label: 'Memory Usage',
type: 'line',
data: {
labels: [],
datasets: [{
label: 'Peak Ram usage',
borderColor: window.chartColors.red,
data: [],
},
{
label: 'Current Ram usage',
borderColor: window.chartColors.blue,
data: [],
}],
},
options: {
responsive: true,
maintainAspectRatio: false,
title: {
display: true,
text: 'Memory Usage',
},
scales: {
x: {
type: 'timeseries',
offset: true,
ticks: {
major: { enabled: true },
fontStyle: context => context.tick.major ? 'bold' : undefined,
source: 'data',
maxRotation: 0,
autoSkip: true,
autoSkipPadding: 75,
}
}
}
}
});
}
function addData(chart, label, newdata) {
chart.data.labels.push(label);
chart.data.datasets.forEach((dataset) => {
dataset.data.push(newdata);
});
chart.update();
}
function updateDualData(chart, label, first, second) {
chart.data.labels.push(label);
chart.data.datasets[0].data.push(first);
chart.data.datasets[1].data.push(second);
chart.update();
}
function updateQuadData(chart, label, first, second, third, fourth) {
chart.data.labels.push(label);
chart.data.datasets[0].data.push(first);
chart.data.datasets[1].data.push(second);
chart.data.datasets[2].data.push(third);
chart.data.datasets[3].data.push(fourth);
chart.update();
}
function update_stats( data ) {
our_callsign = data["APRSDStats"]["callsign"];
$("#version").text( data["APRSDStats"]["version"] );
$("#aprs_connection").html( data["aprs_connection"] );
$("#uptime").text( "uptime: " + data["APRSDStats"]["uptime"] );
const html_pretty = Prism.highlight(JSON.stringify(data, null, '\t'), Prism.languages.json, 'json');
$("#jsonstats").html(html_pretty);
short_time = data["time"].split(/\s(.+)/)[1];
packet_list = data["PacketList"]["packets"];
updateDualData(packets_chart, short_time, data["PacketList"]["sent"], data["PacketList"]["received"]);
updateQuadData(message_chart, short_time, packet_list["MessagePacket"]["tx"], packet_list["MessagePacket"]["rx"],
packet_list["AckPacket"]["tx"], packet_list["AckPacket"]["rx"]);
updateDualData(email_chart, short_time, data["EmailStats"]["sent"], data["EmailStats"]["recieved"]);
updateDualData(memory_chart, short_time, data["APRSDStats"]["memory_peak"], data["APRSDStats"]["memory_current"]);
}

View File

@ -1,465 +0,0 @@
var packet_list = {};
var tx_data = [];
var rx_data = [];
var packet_types_data = {};
var mem_current = []
var mem_peak = []
var thread_current = []
function start_charts() {
console.log("start_charts() called");
// Initialize the echarts instance based on the prepared dom
create_packets_chart();
create_packets_types_chart();
create_messages_chart();
create_ack_chart();
create_memory_chart();
create_thread_chart();
}
function create_packets_chart() {
// The packets totals TX/RX chart.
pkt_c_canvas = document.getElementById('packetsChart');
packets_chart = echarts.init(pkt_c_canvas);
// Specify the configuration items and data for the chart
var option = {
title: {
text: 'APRS Packet totals'
},
legend: {},
tooltip : {
trigger: 'axis'
},
toolbox: {
show : true,
feature : {
mark : {show: true},
dataView : {show: true, readOnly: true},
magicType : {show: true, type: ['line', 'bar']},
restore : {show: true},
saveAsImage : {show: true}
}
},
calculable : true,
xAxis: { type: 'time' },
yAxis: { },
series: [
{
name: 'tx',
type: 'line',
smooth: true,
color: 'red',
encode: {
x: 'timestamp',
y: 'tx' // refer sensor 1 value
}
},{
name: 'rx',
type: 'line',
smooth: true,
encode: {
x: 'timestamp',
y: 'rx'
}
}]
};
// Display the chart using the configuration items and data just specified.
packets_chart.setOption(option);
}
function create_packets_types_chart() {
// The packets types chart
pkt_types_canvas = document.getElementById('packetTypesChart');
packet_types_chart = echarts.init(pkt_types_canvas);
// The series and data are built and updated on the fly
// as packets come in.
var option = {
title: {
text: 'Packet Types'
},
legend: {},
tooltip : {
trigger: 'axis'
},
toolbox: {
show : true,
feature : {
mark : {show: true},
dataView : {show: true, readOnly: true},
magicType : {show: true, type: ['line', 'bar']},
restore : {show: true},
saveAsImage : {show: true}
}
},
calculable : true,
xAxis: { type: 'time' },
yAxis: { },
}
packet_types_chart.setOption(option);
}
function create_messages_chart() {
msg_c_canvas = document.getElementById('messagesChart');
message_chart = echarts.init(msg_c_canvas);
// Specify the configuration items and data for the chart
var option = {
title: {
text: 'Message Packets'
},
legend: {},
tooltip: {
trigger: 'axis'
},
toolbox: {
show: true,
feature: {
mark : {show: true},
dataView : {show: true, readOnly: true},
magicType : {show: true, type: ['line', 'bar']},
restore : {show: true},
saveAsImage : {show: true}
}
},
calculable: true,
xAxis: { type: 'time' },
yAxis: { },
series: [
{
name: 'tx',
type: 'line',
smooth: true,
color: 'red',
encode: {
x: 'timestamp',
y: 'tx' // refer sensor 1 value
}
},{
name: 'rx',
type: 'line',
smooth: true,
encode: {
x: 'timestamp',
y: 'rx'
}
}]
};
// Display the chart using the configuration items and data just specified.
message_chart.setOption(option);
}
function create_ack_chart() {
ack_canvas = document.getElementById('acksChart');
ack_chart = echarts.init(ack_canvas);
// Specify the configuration items and data for the chart
var option = {
title: {
text: 'Ack Packets'
},
legend: {},
tooltip: {
trigger: 'axis'
},
toolbox: {
show: true,
feature: {
mark : {show: true},
dataView : {show: true, readOnly: false},
magicType : {show: true, type: ['line', 'bar']},
restore : {show: true},
saveAsImage : {show: true}
}
},
calculable: true,
xAxis: { type: 'time' },
yAxis: { },
series: [
{
name: 'tx',
type: 'line',
smooth: true,
color: 'red',
encode: {
x: 'timestamp',
y: 'tx' // refer sensor 1 value
}
},{
name: 'rx',
type: 'line',
smooth: true,
encode: {
x: 'timestamp',
y: 'rx'
}
}]
};
ack_chart.setOption(option);
}
function create_memory_chart() {
ack_canvas = document.getElementById('memChart');
memory_chart = echarts.init(ack_canvas);
// Specify the configuration items and data for the chart
var option = {
title: {
text: 'Memory Usage'
},
legend: {},
tooltip: {
trigger: 'axis'
},
toolbox: {
show: true,
feature: {
mark : {show: true},
dataView : {show: true, readOnly: false},
magicType : {show: true, type: ['line', 'bar']},
restore : {show: true},
saveAsImage : {show: true}
}
},
calculable: true,
xAxis: { type: 'time' },
yAxis: { },
series: [
{
name: 'current',
type: 'line',
smooth: true,
color: 'red',
encode: {
x: 'timestamp',
y: 'current' // refer sensor 1 value
}
},{
name: 'peak',
type: 'line',
smooth: true,
encode: {
x: 'timestamp',
y: 'peak'
}
}]
};
memory_chart.setOption(option);
}
function create_thread_chart() {
thread_canvas = document.getElementById('threadChart');
thread_chart = echarts.init(thread_canvas);
// Specify the configuration items and data for the chart
var option = {
title: {
text: 'Active Threads'
},
legend: {},
tooltip: {
trigger: 'axis'
},
toolbox: {
show: true,
feature: {
mark : {show: true},
dataView : {show: true, readOnly: false},
magicType : {show: true, type: ['line', 'bar']},
restore : {show: true},
saveAsImage : {show: true}
}
},
calculable: true,
xAxis: { type: 'time' },
yAxis: { },
series: [
{
name: 'current',
type: 'line',
smooth: true,
color: 'red',
encode: {
x: 'timestamp',
y: 'current' // refer sensor 1 value
}
}
]
};
thread_chart.setOption(option);
}
function updatePacketData(chart, time, first, second) {
tx_data.push([time, first]);
rx_data.push([time, second]);
option = {
series: [
{
name: 'tx',
data: tx_data,
},
{
name: 'rx',
data: rx_data,
}
]
}
chart.setOption(option);
}
function updatePacketTypesData(time, typesdata) {
//The options series is created on the fly each time based on
//the packet types we have in the data
var series = []
for (const k in typesdata) {
tx = [time, typesdata[k]["tx"]]
rx = [time, typesdata[k]["rx"]]
if (packet_types_data.hasOwnProperty(k)) {
packet_types_data[k]["tx"].push(tx)
packet_types_data[k]["rx"].push(rx)
} else {
packet_types_data[k] = {'tx': [tx], 'rx': [rx]}
}
}
}
function updatePacketTypesChart() {
series = []
for (const k in packet_types_data) {
entry = {
name: k+"tx",
data: packet_types_data[k]["tx"],
type: 'line',
smooth: true,
encode: {
x: 'timestamp',
y: k+'tx' // refer sensor 1 value
}
}
series.push(entry)
entry = {
name: k+"rx",
data: packet_types_data[k]["rx"],
type: 'line',
smooth: true,
encode: {
x: 'timestamp',
y: k+'rx' // refer sensor 1 value
}
}
series.push(entry)
}
option = {
series: series
}
packet_types_chart.setOption(option);
}
function updateTypeChart(chart, key) {
//Generic function to update a packet type chart
if (! packet_types_data.hasOwnProperty(key)) {
return;
}
if (! packet_types_data[key].hasOwnProperty('tx')) {
return;
}
var option = {
series: [{
name: "tx",
data: packet_types_data[key]["tx"],
},
{
name: "rx",
data: packet_types_data[key]["rx"]
}]
}
chart.setOption(option);
}
function updateMemChart(time, current, peak) {
mem_current.push([time, current]);
mem_peak.push([time, peak]);
option = {
series: [
{
name: 'current',
data: mem_current,
},
{
name: 'peak',
data: mem_peak,
}
]
}
memory_chart.setOption(option);
}
function updateThreadChart(time, threads) {
keys = Object.keys(threads);
thread_count = keys.length;
thread_current.push([time, thread_count]);
option = {
series: [
{
name: 'current',
data: thread_current,
}
]
}
thread_chart.setOption(option);
}
function updateMessagesChart() {
updateTypeChart(message_chart, "MessagePacket")
}
function updateAcksChart() {
updateTypeChart(ack_chart, "AckPacket")
}
function update_stats( data ) {
console.log("update_stats() echarts.js called")
stats = data["stats"];
our_callsign = stats["APRSDStats"]["callsign"];
$("#version").text( stats["APRSDStats"]["version"] );
$("#aprs_connection").html( stats["aprs_connection"] );
$("#uptime").text( "uptime: " + stats["APRSDStats"]["uptime"] );
const html_pretty = Prism.highlight(JSON.stringify(data, null, '\t'), Prism.languages.json, 'json');
$("#jsonstats").html(html_pretty);
t = Date.parse(data["time"]);
ts = new Date(t);
updatePacketData(packets_chart, ts, stats["PacketList"]["tx"], stats["PacketList"]["rx"]);
updatePacketTypesData(ts, stats["PacketList"]["types"]);
updatePacketTypesChart();
updateMessagesChart();
updateAcksChart();
updateMemChart(ts, stats["APRSDStats"]["memory_current"], stats["APRSDStats"]["memory_peak"]);
updateThreadChart(ts, stats["APRSDThreadList"]);
//updateQuadData(message_chart, short_time, data["stats"]["messages"]["sent"], data["stats"]["messages"]["received"], data["stats"]["messages"]["ack_sent"], data["stats"]["messages"]["ack_recieved"]);
//updateDualData(email_chart, short_time, data["stats"]["email"]["sent"], data["stats"]["email"]["recieved"]);
//updateDualData(memory_chart, short_time, data["stats"]["aprsd"]["memory_peak"], data["stats"]["aprsd"]["memory_current"]);
}

View File

@ -1,26 +0,0 @@
function init_logs() {
const socket = io("/logs");
socket.on('connect', function () {
console.log("Connected to logs socketio");
});
socket.on('connected', function(msg) {
console.log("Connected to /logs");
console.log(msg);
});
socket.on('log_entry', function(data) {
update_logs(data);
});
};
function update_logs(data) {
var code_block = $('#logtext')
entry = data["message"]
const html_pretty = Prism.highlight(entry, Prism.languages.log, 'log');
code_block.append(html_pretty + "<br>");
var div = document.getElementById('logContainer');
div.scrollTop = div.scrollHeight;
}

View File

@ -1,231 +0,0 @@
// watchlist is a dict of ham callsign => symbol, packets
var watchlist = {};
var our_callsign = "";
function aprs_img(item, x_offset, y_offset) {
var x = x_offset * -16;
if (y_offset > 5) {
y_offset = 5;
}
var y = y_offset * -16;
var loc = x + 'px '+ y + 'px'
item.css('background-position', loc);
}
function show_aprs_icon(item, symbol) {
var offset = ord(symbol) - 33;
var col = Math.floor(offset / 16);
var row = offset % 16;
//console.log("'" + symbol+"' off: "+offset+" row: "+ row + " col: " + col)
aprs_img(item, row, col);
}
function ord(str){return str.charCodeAt(0);}
function update_watchlist( data ) {
// Update the watch list
stats = data["stats"];
if (stats.hasOwnProperty("WatchList") == false) {
return
}
var watchdiv = $("#watchDiv");
var html_str = '<table class="ui celled striped table"><thead><tr><th>HAM Callsign</th><th>Age since last seen by APRSD</th></tr></thead><tbody>'
watchdiv.html('')
jQuery.each(stats["WatchList"], function(i, val) {
html_str += '<tr><td class="collapsing"><img id="callsign_'+i+'" class="aprsd_1"></img>' + i + '</td><td>' + val["last"] + '</td></tr>'
});
html_str += "</tbody></table>";
watchdiv.append(html_str);
jQuery.each(watchlist, function(i, val) {
//update the symbol
var call_img = $('#callsign_'+i);
show_aprs_icon(call_img, val['symbol'])
});
}
function update_watchlist_from_packet(callsign, val) {
if (!watchlist.hasOwnProperty(callsign)) {
watchlist[callsign] = {
"symbol": '[',
"packets": {},
}
} else {
if (val.hasOwnProperty('symbol')) {
//console.log("Updating symbol for "+callsign + " to "+val["symbol"])
watchlist[callsign]["symbol"] = val["symbol"]
}
}
if (watchlist[callsign]["packets"].hasOwnProperty(val['ts']) == false) {
watchlist[callsign]["packets"][val['ts']]= val;
}
//console.log(watchlist)
}
function update_seenlist( data ) {
stats = data["stats"];
if (stats.hasOwnProperty("SeenList") == false) {
return
}
var seendiv = $("#seenDiv");
var html_str = '<table class="ui celled striped table">'
html_str += '<thead><tr><th>HAM Callsign</th><th>Age since last seen by APRSD</th>'
html_str += '<th>Number of packets RX</th></tr></thead><tbody>'
seendiv.html('')
var seen_list = stats["SeenList"]
var len = Object.keys(seen_list).length
$('#seen_count').html(len)
jQuery.each(seen_list, function(i, val) {
html_str += '<tr><td class="collapsing">'
html_str += '<img id="callsign_'+i+'" class="aprsd_1"></img>' + i + '</td>'
html_str += '<td>' + val["last"] + '</td>'
html_str += '<td>' + val["count"] + '</td></tr>'
});
html_str += "</tbody></table>";
seendiv.append(html_str);
}
function update_plugins( data ) {
stats = data["stats"];
if (stats.hasOwnProperty("PluginManager") == false) {
return
}
var plugindiv = $("#pluginDiv");
var html_str = '<table class="ui celled striped table"><thead><tr>'
html_str += '<th>Plugin Name</th><th>Plugin Enabled?</th>'
html_str += '<th>Processed Packets</th><th>Sent Packets</th>'
html_str += '<th>Version</th>'
html_str += '</tr></thead><tbody>'
plugindiv.html('')
var plugins = stats["PluginManager"];
var keys = Object.keys(plugins);
keys.sort();
for (var i=0; i<keys.length; i++) { // now lets iterate in sort order
var key = keys[i];
var val = plugins[key];
html_str += '<tr><td class="collapsing">' + key + '</td>';
html_str += '<td>' + val["enabled"] + '</td><td>' + val["rx"] + '</td>';
html_str += '<td>' + val["tx"] + '</td><td>' + val["version"] +'</td></tr>';
}
html_str += "</tbody></table>";
plugindiv.append(html_str);
}
function update_threads( data ) {
stats = data["stats"];
if (stats.hasOwnProperty("APRSDThreadList") == false) {
return
}
var threadsdiv = $("#threadsDiv");
var countdiv = $("#thread_count");
var html_str = '<table class="ui celled striped table"><thead><tr>'
html_str += '<th>Thread Name</th><th>Alive?</th>'
html_str += '<th>Age</th><th>Loop Count</th>'
html_str += '</tr></thead><tbody>'
threadsdiv.html('')
var threads = stats["APRSDThreadList"];
var keys = Object.keys(threads);
countdiv.html(keys.length);
keys.sort();
for (var i=0; i<keys.length; i++) { // now lets iterate in sort order
var key = keys[i];
var val = threads[key];
html_str += '<tr><td class="collapsing">' + key + '</td>';
html_str += '<td>' + val["alive"] + '</td><td>' + val["age"] + '</td>';
html_str += '<td>' + val["loop_count"] + '</td></tr>';
}
html_str += "</tbody></table>";
threadsdiv.append(html_str);
}
function update_packets( data ) {
var packetsdiv = $("#packetsDiv");
//nuke the contents first, then add to it.
if (size_dict(packet_list) == 0 && size_dict(data) > 0) {
packetsdiv.html('')
}
jQuery.each(data.packets, function(i, val) {
pkt = val;
update_watchlist_from_packet(pkt['from_call'], pkt);
if ( packet_list.hasOwnProperty(pkt['timestamp']) == false ) {
// Store the packet
packet_list[pkt['timestamp']] = pkt;
//ts_str = val["timestamp"].toString();
//ts = ts_str.split(".")[0]*1000;
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';
} else {
title_id = 'title_rx';
}
var from_to = d + " " + t + "&nbsp;&nbsp;&nbsp;&nbsp;" + from_call + " > "
if (val.hasOwnProperty('addresse')) {
from_to = from_to + pkt['addresse']
} else if (pkt.hasOwnProperty('to_call')) {
from_to = from_to + pkt['to_call']
} else if (pkt.hasOwnProperty('format') && pkt['format'] == 'mic-e') {
from_to = from_to + "Mic-E"
}
from_to = from_to + "&nbsp;&nbsp;-&nbsp;&nbsp;" + pkt['raw']
json_pretty = Prism.highlight(JSON.stringify(pkt, null, '\t'), Prism.languages.json, 'json');
pkt_html = '<div class="title" id="' + title_id + '"><i class="dropdown icon"></i>' + from_to + '</div><div class="content"><p class="transition hidden"><pre class="language-json">' + json_pretty + '</p></p></div>'
packetsdiv.prepend(pkt_html);
}
});
$('.ui.accordion').accordion('refresh');
// Update the count of messages shown
cnt = size_dict(packet_list);
//console.log("packets list " + cnt)
$('#packets_count').html(cnt);
const html_pretty = Prism.highlight(JSON.stringify(data, null, '\t'), Prism.languages.json, 'json');
$("#packetsjson").html(html_pretty);
}
function start_update() {
(function statsworker() {
$.ajax({
url: "/stats",
type: 'GET',
dataType: 'json',
success: function(data) {
update_stats(data);
update_watchlist(data);
update_seenlist(data);
update_plugins(data);
update_threads(data);
},
complete: function() {
setTimeout(statsworker, 10000);
}
});
})();
(function packetsworker() {
$.ajax({
url: "/packets",
type: 'GET',
dataType: 'json',
success: function(data) {
update_packets(data);
},
complete: function() {
setTimeout(packetsworker, 10000);
}
});
})();
}

File diff suppressed because one or more lines are too long

View File

@ -1,114 +0,0 @@
var cleared = false;
function size_dict(d){c=0; for (i in d) ++c; return c}
function init_messages() {
const socket = io("/sendmsg");
socket.on('connect', function () {
console.log("Connected to socketio");
});
socket.on('connected', function(msg) {
console.log("Connected!");
console.log(msg);
});
socket.on("sent", function(msg) {
if (cleared == false) {
var msgsdiv = $("#msgsDiv");
msgsdiv.html('')
cleared = true
}
add_msg(msg);
});
socket.on("ack", function(msg) {
update_msg(msg);
});
socket.on("reply", function(msg) {
update_msg(msg);
});
}
function add_msg(msg) {
var msgsdiv = $("#sendMsgsDiv");
ts_str = msg["ts"].toString();
ts = ts_str.split(".")[0]*1000;
var d = new Date(ts).toLocaleDateString("en-US")
var t = new Date(ts).toLocaleTimeString("en-US")
from = msg['from']
title_id = 'title_tx'
var from_to = d + " " + t + "&nbsp;&nbsp;&nbsp;&nbsp;" + from + " > "
if (msg.hasOwnProperty('to')) {
from_to = from_to + msg['to']
}
from_to = from_to + "&nbsp;&nbsp;-&nbsp;&nbsp;" + msg['message']
id = ts_str.split('.')[0]
pretty_id = "pretty_" + id
loader_id = "loader_" + id
ack_id = "ack_" + id
reply_id = "reply_" + id
span_id = "span_" + id
json_pretty = Prism.highlight(JSON.stringify(msg, null, '\t'), Prism.languages.json, 'json');
msg_html = '<div class="ui title" id="' + title_id + '"><i class="dropdown icon"></i>';
msg_html += '<div class="ui active inline loader" id="' + loader_id +'" data-content="Waiting for Ack"></div>&nbsp;';
msg_html += '<i class="thumbs down outline icon" id="' + ack_id + '" data-content="Waiting for ACK"></i>&nbsp;';
msg_html += '<i class="thumbs down outline icon" id="' + reply_id + '" data-content="Waiting for Reply"></i>&nbsp;';
msg_html += '<span id="' + span_id + '">' + from_to +'</span></div>';
msg_html += '<div class="content"><p class="transition hidden"><pre id="' + pretty_id + '" class="language-json">' + json_pretty + '</p></p></div>'
msgsdiv.prepend(msg_html);
$('.ui.accordion').accordion('refresh');
}
function update_msg(msg) {
var msgsdiv = $("#sendMsgsDiv");
// We have an existing entry
ts_str = msg["ts"].toString();
id = ts_str.split('.')[0]
pretty_id = "pretty_" + id
loader_id = "loader_" + id
reply_id = "reply_" + id
ack_id = "ack_" + id
span_id = "span_" + id
if (msg['ack'] == true) {
var loader_div = $('#' + loader_id);
var ack_div = $('#' + ack_id);
loader_div.removeClass('ui active inline loader');
loader_div.addClass('ui disabled loader');
ack_div.removeClass('thumbs up outline icon');
ack_div.addClass('thumbs up outline icon');
}
if (msg['reply'] !== null) {
var reply_div = $('#' + reply_id);
reply_div.removeClass("thumbs down outline icon");
reply_div.addClass('reply icon');
reply_div.attr('data-content', 'Got Reply');
var d = new Date(ts).toLocaleDateString("en-US")
var t = new Date(ts).toLocaleTimeString("en-US")
var from_to = d + " " + t + "&nbsp;&nbsp;&nbsp;&nbsp;" + from + " > "
if (msg.hasOwnProperty('to')) {
from_to = from_to + msg['to']
}
from_to = from_to + "&nbsp;&nbsp;-&nbsp;&nbsp;" + msg['message']
from_to += "&nbsp;&nbsp; ===> " + msg["reply"]["message_text"]
var span_div = $('#' + span_id);
span_div.html(from_to);
}
var pretty_pre = $("#" + pretty_id);
pretty_pre.html('');
json_pretty = Prism.highlight(JSON.stringify(msg, null, '\t'), Prism.languages.json, 'json');
pretty_pre.html(json_pretty);
$('.ui.accordion').accordion('refresh');
}

View File

@ -1,28 +0,0 @@
function openTab(evt, tabName) {
// Declare all variables
var i, tabcontent, tablinks;
if (typeof tabName == 'undefined') {
return
}
// Get all elements with class="tabcontent" and hide them
tabcontent = document.getElementsByClassName("tabcontent");
for (i = 0; i < tabcontent.length; i++) {
tabcontent[i].style.display = "none";
}
// Get all elements with class="tablinks" and remove the class "active"
tablinks = document.getElementsByClassName("tablinks");
for (i = 0; i < tablinks.length; i++) {
tablinks[i].className = tablinks[i].className.replace(" active", "");
}
// Show the current tab, and add an "active" class to the button that opened the tab
document.getElementById(tabName).style.display = "block";
if (typeof evt.currentTarget == 'undefined') {
return
} else {
evt.currentTarget.className += " active";
}
}

View File

@ -1,196 +0,0 @@
<html>
<head>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
<link rel="stylesheet" href="https://ajax.googleapis.com/ajax/libs/jqueryui/1.12.1/themes/smoothness/jquery-ui.css">
<script src="https://ajax.googleapis.com/ajax/libs/jqueryui/1.12.1/jquery-ui.min.js"></script>
<script src="https://cdn.socket.io/4.7.1/socket.io.min.js" integrity="sha512-+NaO7d6gQ1YPxvc/qHIqZEchjGm207SszoNeMgppoqD/67fEqmc1edS8zrbxPD+4RQI3gDgT/83ihpFW61TG/Q==" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/chart.js@2.9.4/dist/Chart.bundle.js"></script>
<script src="https://cdn.jsdelivr.net/npm/echarts@5.4.3/dist/echarts.min.js"></script>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/semantic-ui@2.4.2/dist/semantic.min.css">
<script src="https://cdn.jsdelivr.net/npm/semantic-ui@2.4.2/dist/semantic.min.js"></script>
<link rel="stylesheet" href="/static/css/index.css">
<link rel="stylesheet" href="/static/css/tabs.css">
<link rel="stylesheet" href="/static/css/prism.css">
<script src="/static/js/prism.js"></script>
<script src="/static/js/main.js"></script>
<script src="/static/js/echarts.js"></script>
<script src="/static/js/tabs.js"></script>
<script src="/static/js/send-message.js"></script>
<script src="/static/js/logs.js"></script>
<script type="text/javascript">
var initial_stats = {{ initial_stats|tojson|safe }};
var memory_chart = null
var message_chart = null
var color = Chart.helpers.color;
$(document).ready(function() {
start_update();
start_charts();
init_messages();
init_logs();
$("#toggleStats").click(function() {
$("#jsonstats").fadeToggle(1000);
});
// Pretty print the config json so it's readable
var cfg_data = $("#configjson").text();
var cfg_json = JSON.parse(cfg_data);
var cfg_pretty = JSON.stringify(cfg_json, null, '\t');
const html_pretty = Prism.highlight( cfg_pretty, Prism.languages.json, 'json');
$("#configjson").html(html_pretty);
$("#jsonstats").fadeToggle(1000);
//var log_text_pretty = $('#logtext').text();
//const log_pretty = Prism.highlight( log_text_pretty, Prism.languages.log, 'log');
//$('#logtext').html(log_pretty);
$('.ui.accordion').accordion({exclusive: false});
$('.menu .item').tab('change tab', 'charts-tab');
});
</script>
</head>
<body>
<div class='ui text container'>
<h1 class='ui dividing header'>APRSD {{ version }}</h1>
</div>
<div class='ui grid text container'>
<div class='left floated ten wide column'>
<span style='color: green'>{{ callsign }}</span>
connected to
<span style='color: blue' id='aprs_connection'>{{ aprs_connection|safe }}</span>
</div>
<div class='right floated four wide column'>
<span id='uptime'>NONE</span>
</div>
</div>
<!-- Tab links -->
<div class="ui top attached tabular menu">
<div class="active item" data-tab="charts-tab">Charts</div>
<div class="item" data-tab="msgs-tab">Messages</div>
<div class="item" data-tab="seen-tab">Seen List</div>
<div class="item" data-tab="watch-tab">Watch List</div>
<div class="item" data-tab="plugin-tab">Plugins</div>
<div class="item" data-tab="threads-tab">Threads</div>
<div class="item" data-tab="config-tab">Config</div>
<div class="item" data-tab="log-tab">LogFile</div>
<!-- <div class="item" data-tab="oslo-tab">OSLO CONFIG</div> //-->
<div class="item" data-tab="raw-tab">Raw JSON</div>
</div>
<!-- Tab content -->
<div class="ui bottom attached active tab segment" data-tab="charts-tab">
<h3 class="ui dividing header">Charts</h3>
<div class="ui equal width relaxed grid">
<div class="row">
<div class="column">
<div class="ui segment" style="height: 300px" id="packetsChart"></div>
</div>
</div>
<div class="row">
<div class="column">
<div class="ui segment" style="height: 300px" id="messagesChart"></div>
</div>
<div class="column">
<div class="ui segment" style="height: 300px" id="acksChart"></div>
</div>
</div>
<div class="row">
<div class="column">
<div class="ui segment" style="height: 300px" id="packetTypesChart"></div>
</div>
</div>
<div class="row">
<div class="column">
<div class="ui segment" style="height: 300px" id="threadChart"></div>
</div>
</div>
<div class="row">
<div class="column">
<div class="ui segment" style="height: 300px" id="memChart"></div>
</div>
</div>
<!-- <div class="row">
<div id="stats" class="two column">
<button class="ui button" id="toggleStats">Toggle raw json</button>
<pre id="jsonstats" class="language-json">{{ stats }}</pre>
</div> //-->
</div>
</div>
</div>
<div class="ui bottom attached tab segment" data-tab="msgs-tab">
<h3 class="ui dividing header">Messages (<span id="packets_count">0</span>)</h3>
<div class="ui styled fluid accordion" id="accordion">
<div id="packetsDiv" class="ui mini text">Loading</div>
</div>
</div>
<div class="ui bottom attached tab segment" data-tab="seen-tab">
<h3 class="ui dividing header">
Callsign Seen List (<span id="seen_count">{{ seen_count }}</span>)
</h3>
<div id="seenDiv" class="ui mini text">Loading</div>
</div>
<div class="ui bottom attached tab segment" data-tab="watch-tab">
<h3 class="ui dividing header">
Callsign Watch List (<span id="watch_count">{{ watch_count }}</span>)
&nbsp;&nbsp;&nbsp;
Notification age - <span id="watch_age">{{ watch_age }}</span>
</h3>
<div id="watchDiv" class="ui mini text">Loading</div>
</div>
<div class="ui bottom attached tab segment" data-tab="plugin-tab">
<h3 class="ui dividing header">
Plugins Loaded (<span id="plugin_count">{{ plugin_count }}</span>)
</h3>
<div id="pluginDiv" class="ui mini text">Loading</div>
</div>
<div class="ui bottom attached tab segment" data-tab="threads-tab">
<h3 class="ui dividing header">
Threads Loaded (<span id="thread_count">{{ thread_count }}</span>)
</h3>
<div id="threadsDiv" class="ui mini text">Loading</div>
</div>
<div class="ui bottom attached tab segment" data-tab="config-tab">
<h3 class="ui dividing header">Config</h3>
<pre id="configjson" class="language-json">{{ config_json|safe }}</pre>
</div>
<div class="ui bottom attached tab segment" data-tab="log-tab">
<h3 class="ui dividing header">LOGFILE</h3>
<pre id="logContainer" style="height: 600px;overflow-y:auto;overflow-x:auto;"><code id="logtext" class="language-log" ></code></pre>
</div>
<!--
<div class="ui bottom attached tab segment" data-tab="oslo-tab">
<h3 class="ui dividing header">OSLO</h3>
<pre id="osloContainer" style="height:600px;overflow-y:auto;" class="language-json">{{ oslo_out|safe }}</pre>
</div> //-->
<div class="ui bottom attached tab segment" data-tab="raw-tab">
<h3 class="ui dividing header">Raw JSON</h3>
<pre id="jsonstats" class="language-yaml" style="height:600px;overflow-y:auto;">{{ initial_stats|safe }}</pre>
</div>
<div class="ui text container">
<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>

View File

@ -1,115 +0,0 @@
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;
}

View File

@ -1,66 +0,0 @@
body {
background: #eeeeee;
/*margin: 1em;*/
text-align: center;
font-family: system-ui, sans-serif;
height: 100%;
}
#title {
font-size: 4em;
}
#version{
font-size: .5em;
}
#uptime, #aprsis {
font-size: 1em;
}
#callsign {
font-size: 1.4em;
color: #00F;
padding-top: 8px;
margin:10px;
}
#title_rx {
background-color: darkseagreen;
text-align: left;
}
#title_tx {
background-color: lightcoral;
text-align: left;
}
.aprsd_1 {
background-image: url(/static/images/aprs-symbols-16-0.png);
background-repeat: no-repeat;
background-position: -160px -48px;
width: 16px;
height: 16px;
}
.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;
}
.material-symbols-rounded.md-10 {
font-size: 18px !important;
}

File diff suppressed because one or more lines are too long

View File

@ -1,41 +0,0 @@
* {box-sizing: border-box}
/* Style the tab */
.tab {
border: 1px solid #ccc;
background-color: #f1f1f1;
height: 450px;
}
/* Style the buttons inside the tab */
.tab div {
display: block;
background-color: inherit;
color: black;
padding: 10px;
width: 100%;
border: none;
outline: none;
text-align: left;
cursor: pointer;
transition: 0.3s;
font-size: 17px;
}
/* Change background color of buttons on hover */
.tab div:hover {
background-color: #ddd;
}
/* Create an active/current "tab button" class */
.tab div.active {
background-color: #ccc;
}
/* Style the tab content */
.tabcontent {
border: 1px solid #ccc;
height: 450px;
overflow-y: scroll;
background-color: #CCCCCC;
}

File diff suppressed because one or more lines are too long

View File

@ -1,23 +0,0 @@
/* fallback */
@font-face {
font-family: 'Material Symbols Rounded';
font-style: normal;
font-weight: 200;
src: url(/static/css/upstream/font.woff2) format('woff2');
}
.material-symbols-rounded {
font-family: 'Material Symbols Rounded';
font-weight: normal;
font-style: normal;
font-size: 24px;
line-height: 1;
letter-spacing: normal;
text-transform: none;
display: inline-block;
white-space: nowrap;
word-wrap: normal;
direction: ltr;
-webkit-font-feature-settings: 'liga';
-webkit-font-smoothing: antialiased;
}

File diff suppressed because it is too large Load Diff

View File

@ -1,28 +0,0 @@
/**
* 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; }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

View File

@ -1,3 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-globe" viewBox="0 0 16 16">
<path d="M0 8a8 8 0 1 1 16 0A8 8 0 0 1 0 8m7.5-6.923c-.67.204-1.335.82-1.887 1.855A8 8 0 0 0 5.145 4H7.5zM4.09 4a9.3 9.3 0 0 1 .64-1.539 7 7 0 0 1 .597-.933A7.03 7.03 0 0 0 2.255 4zm-.582 3.5c.03-.877.138-1.718.312-2.5H1.674a7 7 0 0 0-.656 2.5zM4.847 5a12.5 12.5 0 0 0-.338 2.5H7.5V5zM8.5 5v2.5h2.99a12.5 12.5 0 0 0-.337-2.5zM4.51 8.5a12.5 12.5 0 0 0 .337 2.5H7.5V8.5zm3.99 0V11h2.653c.187-.765.306-1.608.338-2.5zM5.145 12q.208.58.468 1.068c.552 1.035 1.218 1.65 1.887 1.855V12zm.182 2.472a7 7 0 0 1-.597-.933A9.3 9.3 0 0 1 4.09 12H2.255a7 7 0 0 0 3.072 2.472M3.82 11a13.7 13.7 0 0 1-.312-2.5h-2.49c.062.89.291 1.733.656 2.5zm6.853 3.472A7 7 0 0 0 13.745 12H11.91a9.3 9.3 0 0 1-.64 1.539 7 7 0 0 1-.597.933M8.5 12v2.923c.67-.204 1.335-.82 1.887-1.855q.26-.487.468-1.068zm3.68-1h2.146c.365-.767.594-1.61.656-2.5h-2.49a13.7 13.7 0 0 1-.312 2.5m2.802-3.5a7 7 0 0 0-.656-2.5H12.18c.174.782.282 1.623.312 2.5zM11.27 2.461c.247.464.462.98.64 1.539h1.835a7 7 0 0 0-3.072-2.472c.218.284.418.598.597.933M10.855 4a8 8 0 0 0-.468-1.068C9.835 1.897 9.17 1.282 8.5 1.077V4z"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -1,84 +0,0 @@
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!")
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();
}
});
}
function getLocation() {
if (navigator.geolocation) {
console.log("getCurrentPosition");
try {
navigator.geolocation.getCurrentPosition(
showPosition, showError,
{timeout:3000});
} catch(err) {
console.log("Failed to getCurrentPosition");
console.log(err);
}
} else {
var msg = "Geolocation is not supported by this browser."
console.log(msg);
alert(msg)
}
}
function showError(error) {
console.log("showError");
console.log(error);
var msg = "";
switch(error.code) {
case error.PERMISSION_DENIED:
msg = "User denied the request for Geolocation."
break;
case error.POSITION_UNAVAILABLE:
msg = "Location information is unavailable."
break;
case error.TIMEOUT:
msg = "The location fix timed out."
break;
case error.UNKNOWN_ERROR:
msg = "An unknown error occurred."
break;
}
console.log(msg);
$.toast({
title: 'GPS Error',
class: 'warning',
position: 'middle center',
message: msg,
showProgress: 'top',
classProgress: 'blue',
});
}
function showPosition(position) {
console.log("showPosition Called");
path = $('#pkt_path option:selected').val();
msg = {
'latitude': position.coords.latitude,
'longitude': position.coords.longitude,
'path': path,
}
console.log(msg);
$.toast({
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")
socket.emit("gps", msg);
}

View File

@ -1,45 +0,0 @@
function aprs_img(item, x_offset, y_offset) {
var x = x_offset * -16;
if (y_offset > 5) {
y_offset = 5;
}
var y = y_offset * -16;
var loc = x + 'px '+ y + 'px'
item.css('background-position', loc);
}
function show_aprs_icon(item, symbol) {
var offset = ord(symbol) - 33;
var col = Math.floor(offset / 16);
var row = offset % 16;
//console.log("'" + symbol+"' off: "+offset+" row: "+ row + " col: " + col)
aprs_img(item, row, col);
}
function ord(str){return str.charCodeAt(0);}
function update_stats( data ) {
console.log(data);
$("#version").text( data["stats"]["APRSDStats"]["version"] );
$("#aprs_connection").html( data["aprs_connection"] );
$("#uptime").text( "uptime: " + data["stats"]["APRSDStats"]["uptime"] );
short_time = data["time"].split(/\s(.+)/)[1];
}
function start_update() {
(function statsworker() {
$.ajax({
url: "/stats",
type: 'GET',
dataType: 'json',
success: function(data) {
update_stats(data);
},
complete: function() {
setTimeout(statsworker, 60000);
}
});
})();
}

View File

@ -1,612 +0,0 @@
var cleared = false;
var callsign_list = {};
var callsign_location = {};
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 reload_popovers() {
$('[data-bs-toggle="popover"]').popover(
{html: true, animation: true}
);
}
function build_location_string(msg) {
dt = new Date(parseInt(msg['lasttime']) * 1000);
loc = "Last Location Update: " + dt.toLocaleString();
loc += "<br>Latitude: " + msg['lat'] + "<br>Longitude: " + msg['lon'];
loc += "<br>" + "Altitude: " + msg['altitude'] + " m";
loc += "<br>" + "Speed: " + msg['speed'] + " kph";
loc += "<br>" + "Bearing: " + msg['compass_bearing'];
loc += "<br>" + "distance: " + msg['distance'] + " km";
return loc;
}
function build_location_string_small(msg) {
dt = new Date(parseInt(msg['lasttime']) * 1000);
loc = "" + msg['distance'] + "km";
//loc += "Lat " + msg['lat'] + "&nbsp;Lon " + msg['lon'];
loc += "&nbsp;" + msg['compass_bearing'];
//loc += "&nbsp;Distance " + msg['distance'] + " km";
//loc += "&nbsp;" + dt.toLocaleString();
loc += "&nbsp;" + msg['timeago'];
return loc;
}
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");
});
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;
}
msg["type"] = MSG_TYPE_TX;
sent_msg(msg);
});
socket.on("ack", function(msg) {
msg["type"] = MSG_TYPE_ACK;
ack_msg(msg);
});
socket.on("new", function(msg) {
if (cleared === false) {
var msgsdiv = $("#msgsTabsDiv");
msgsdiv.html('')
cleared = true;
}
msg["type"] = MSG_TYPE_RX;
from_msg(msg);
});
socket.on("callsign_location", function(msg) {
console.log("CALLSIGN Location!");
console.log(msg);
now = new Date();
msg['last_updated'] = now;
callsign_location[msg['callsign']] = msg;
location_id = callsign_location_content(msg['callsign'], true);
location_string = build_location_string_small(msg);
$(location_id).html(location_string);
$(location_id+"Spinner").addClass('d-none');
save_data();
});
$("#sendform").submit(function(event) {
event.preventDefault();
to_call = $('#to_call').val().toUpperCase();
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('');
callsign_select(to_call);
activate_callsign_tab(to_call);
}
});
init_gps();
// Try and load any existing chat threads from last time
init_messages();
}
function tab_string(callsign, id=false) {
name = "msgs"+callsign;
if (id) {
return "#"+name;
} else {
return name;
}
}
function tab_li_string(callsign, id=false) {
//The id of the LI containing the tab
return tab_string(callsign,id)+"Li";
}
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, 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 callsign_tab(callsign) {
return "#"+tab_string(callsign);
}
function callsign_location_popover(callsign, id=false) {
return tab_string(callsign, id)+"Location";
}
function callsign_location_content(callsign, id=false) {
return tab_string(callsign, id)+"LocationContent";
}
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 {
return name;
}
}
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];
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};
}
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));
localStorage.setItem('callsign_location', JSON.stringify(callsign_location));
}
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'));
callsign_location = JSON.parse(localStorage.getItem('callsign_location'));
if (callsign_list == null) {
callsign_list = {};
}
if (message_list == null) {
message_list = {};
}
if (callsign_location == null) {
callsign_location = {};
}
console.log(callsign_list);
console.log(message_list);
console.log(callsign_location);
// 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);
popover_id = callsign_location_popover(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+'&nbsp;&nbsp;';
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 = '';
}
location_str = "Unknown Location"
if (callsign in callsign_location) {
location_str = build_location_string_small(callsign_location[callsign]);
location_class = '';
}
location_id = callsign_location_content(callsign);
item_html = '<div class="tab-pane fade '+active_str+'" id="'+tab_content+'" role="tabpanel" aria-labelledby="'+tab_id+'">';
item_html += '<div class="" style="border: 1px solid #999999;background-color:#aaaaaa;">';
item_html += '<div class="row" style="padding-top:4px;padding-bottom:4px;background-color:#aaaaaa;margin:0px;">';
item_html += '<div class="d-flex col-md-10 justify-content-left" style="padding:0px;margin:0px;">';
item_html += '<button onclick="call_callsign_location(\''+callsign+'\');" style="margin-left:2px;padding: 0px 4px 0px 4px;font-size: .9rem" type="button" class="btn btn-primary">';
item_html += '<span id="'+location_id+'Spinner" class="d-none spinner-border spinner-border-sm" role="status" aria-hidden="true" style="font-size: .9rem"></span>Update</button>';
item_html += '&nbsp;<span id="'+location_id+'" style="font-size: .9rem">'+location_str+'</span></div>';
item_html += '</div>';
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];
delete callsign_location[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, msg) {
//Get the selected path to save for this callsign
path = msg['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] = {};
}
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());
if (isNaN(count)) {
count = 0;
}
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);
append_message_html(callsign, msg_html, new_callsign);
len = Object.keys(callsign_list).length;
if (new_callsign) {
//Now click the tab if and only if there is only one 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(":","");
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 = '<div class="bubble-row'+alt+'">';
msg_html += '<div id="'+bubble_msgid+'" class="'+ bubble_class + '" ';
msg_html += 'title="APRS Raw Packet" data-bs-placement="'+popover_placement+'" data-bs-toggle="popover" ';
msg_html += 'data-bs-trigger="hover" data-bs-content="'+msg['raw']+'">';
msg_html += '<div class="bubble-text">';
msg_html += '<p class="'+ bubble_name_class +'">'+from+'&nbsp;&nbsp;';
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>";
msg_html += '<p class="' +bubble_msg_class+ '">'+message+'</p>';
msg_html += '<div class="'+ bubble_arrow_class + '"></div>';
msg_html += "</div></div></div>";
return msg_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['to_call']);
reload_popovers();
}
function str_to_int(my_string) {
total = 0
for (let i = 0; i < my_string.length; i++) {
total += my_string.charCodeAt(i);
}
return total
}
function from_msg(msg) {
if (!from_msg_list.hasOwnProperty(msg["from_call"])) {
from_msg_list[msg["from_call"]] = new Array();
}
// Try to account for messages that have no msgNo
console.log(msg)
if (msg["msgNo"] == null) {
console.log("Need to add msgNO!!")
// create an artificial msgNo
total = str_to_int(msg["from_call"])
total += str_to_int(msg["addresse"])
total += str_to_int(msg["message_text"])
msg["msgNo"] = total
}
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 {
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'];
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);
reload_popovers();
}
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');
}
//$('.ui.accordion').accordion('refresh');
save_data();
scroll_main_content();
}
function activate_callsign_tab(callsign) {
tab_content = tab_string(callsign, id=true);
$(tab_content).click();
}
function callsign_select(callsign) {
var tocall = $("#to_call");
tocall.val(callsign.toUpperCase());
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]);
}
function call_callsign_location(callsign) {
msg = {'callsign': callsign};
socket.emit("get_callsign_location", msg);
location_id = callsign_location_content(callsign, true)+"Spinner";
$(location_id).removeClass('d-none');
}

View File

@ -1,28 +0,0 @@
function openTab(evt, tabName) {
// Declare all variables
var i, tabcontent, tablinks;
if (typeof tabName == 'undefined') {
return
}
// Get all elements with class="tabcontent" and hide them
tabcontent = document.getElementsByClassName("tabcontent");
for (i = 0; i < tabcontent.length; i++) {
tabcontent[i].style.display = "none";
}
// Get all elements with class="tablinks" and remove the class "active"
tablinks = document.getElementsByClassName("tablinks");
for (i = 0; i < tablinks.length; i++) {
tablinks[i].className = tablinks[i].className.replace(" active", "");
}
// Show the current tab, and add an "active" class to the button that opened the tab
document.getElementById(tabName).style.display = "block";
if (typeof evt.currentTarget == 'undefined') {
return
} else {
evt.currentTarget.className += " active";
}
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,374 +0,0 @@
// 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">&times;</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

View File

@ -1,139 +0,0 @@
<html>
<head>
<meta name="viewport"
content="width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1">
<script src="/static/js/upstream/jquery-3.7.1.min.js"></script>
<script src="/static/js/upstream/jquery.toast.js"></script>
<script src="/static/js/upstream/socket.io.min.js"></script>
<link rel="stylesheet" href="/static/css/upstream/bootstrap.min.css">
<script src="/static/js/upstream/bootstrap.bundle.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/upstream/google-fonts.css">
<link rel="stylesheet" href="/static/css/upstream/jquery.toast.css">
<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>
<script type="text/javascript">
var initial_stats = {{ initial_stats|tojson|safe }};
var latitude = parseFloat('{{ latitude|safe }}');
var longitude = parseFloat('{{ longitude|safe }}');
var memory_chart = null;
var message_chart = null;
$(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();
});
// 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;
});
/*$('[data-bs-toggle="popover"]').popover(
{html: true, animation: true}
);*/
reload_popovers();
});
</script>
</head>
<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 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="" disabled 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>
<option value="GATE">GATE</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="wc-row content" id="wc-content">
<div class="container" style="max-width: 800px;">
<div class="tab-content" id="msgsTabContent">
</div>
</div>
</div>
<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>
</div>
</body>
</html>

View File

@ -1,5 +1,5 @@
#
# This file is autogenerated by pip-compile with Python 3.10
# This file is autogenerated by pip-compile with Python 3.12
# by the following command:
#
# pip-compile --annotation-style=line requirements-dev.in
@ -20,10 +20,9 @@ click==8.1.7 # via black, fixit, moreorless, pip-tools
colorama==0.4.6 # via tox
commonmark==0.9.1 # via rich
configargparse==1.7 # via gray
coverage[toml]==7.6.8 # via pytest-cov
coverage[toml]==7.6.9 # via pytest-cov
distlib==0.3.9 # via virtualenv
docutils==0.21.2 # via m2r, sphinx
exceptiongroup==1.2.2 # via pytest
filelock==3.16.1 # via tox, virtualenv
fixit==2.1.0 # via gray
flake8==7.1.1 # via -r requirements-dev.in, pep8-naming
@ -71,10 +70,9 @@ sphinxcontrib-qthelp==2.0.0 # via sphinx
sphinxcontrib-serializinghtml==2.0.0 # via sphinx
tokenize-rt==6.1.0 # via add-trailing-comma, pyupgrade
toml==0.10.2 # via autoflake
tomli==2.2.1 # via black, build, check-manifest, coverage, fixit, mypy, pip-tools, pyproject-api, pytest, sphinx, tox
tox==4.23.2 # via -r requirements-dev.in
trailrunner==1.4.0 # via fixit
typing-extensions==4.12.2 # via black, mypy, tox
typing-extensions==4.12.2 # via mypy
unify==0.5 # via gray
untokenize==0.1.1 # via unify
urllib3==2.2.3 # via requests

View File

@ -2,25 +2,17 @@ aprslib>=0.7.0
# For the list-plugins pypi.org search scraping
beautifulsoup4
click
click-params
dataclasses-json
flask
flask-httpauth
flask-socketio
geopy
imapclient
kiss3
loguru
oslo.config
pluggy
python-socketio
requests
# Pinned due to gray needing 12.6.0
rich~=12.6.0
rush
shellingham
six
tabulate
thesmuggler
tzlocal
update_checker

View File

@ -1,5 +1,5 @@
#
# This file is autogenerated by pip-compile with Python 3.10
# This file is autogenerated by pip-compile with Python 3.12
# by the following command:
#
# pip-compile --annotation-style=line requirements.in
@ -8,31 +8,20 @@ aprslib==0.7.2 # via -r requirements.in
attrs==24.2.0 # via ax253, kiss3, rush
ax253==0.1.5.post1 # via kiss3
beautifulsoup4==4.12.3 # via -r requirements.in
bidict==0.23.1 # via python-socketio
bitarray==3.0.0 # via ax253, kiss3
blinker==1.9.0 # via flask
certifi==2024.8.30 # via requests
charset-normalizer==3.4.0 # via requests
click==8.1.7 # via -r requirements.in, click-params, flask
click-params==0.5.0 # via -r requirements.in
click==8.1.7 # via -r requirements.in
commonmark==0.9.1 # via rich
dataclasses-json==0.6.7 # via -r requirements.in
debtcollector==3.0.0 # via oslo-config
deprecated==1.2.15 # via click-params
flask==3.1.0 # via -r requirements.in, flask-httpauth, flask-socketio
flask-httpauth==4.8.0 # via -r requirements.in
flask-socketio==5.4.1 # via -r requirements.in
geographiclib==2.0 # via geopy
geopy==2.4.1 # via -r requirements.in
h11==0.14.0 # via wsproto
idna==3.10 # via requests
imapclient==3.0.1 # via -r requirements.in
importlib-metadata==8.5.0 # via ax253, kiss3
itsdangerous==2.2.0 # via flask
jinja2==3.1.4 # via flask
kiss3==8.0.0 # via -r requirements.in
loguru==0.7.2 # via -r requirements.in
markupsafe==3.0.2 # via jinja2, werkzeug
loguru==0.7.3 # via -r requirements.in
marshmallow==3.23.1 # via dataclasses-json
mypy-extensions==1.0.0 # via typing-inspect
netaddr==1.3.0 # via oslo-config
@ -44,20 +33,14 @@ pluggy==1.5.0 # via -r requirements.in
pygments==2.18.0 # via rich
pyserial==3.5 # via pyserial-asyncio
pyserial-asyncio==0.6 # via kiss3
python-engineio==4.10.1 # via python-socketio
python-socketio==5.11.4 # via -r requirements.in, flask-socketio
pytz==2024.2 # via -r requirements.in
pyyaml==6.0.2 # via oslo-config
requests==2.32.3 # via -r requirements.in, oslo-config, update-checker
rfc3986==2.0.0 # via oslo-config
rich==12.6.0 # via -r requirements.in
rush==2021.4.0 # via -r requirements.in
shellingham==1.5.4 # via -r requirements.in
simple-websocket==1.1.0 # via python-engineio
six==1.16.0 # via -r requirements.in
soupsieve==2.6 # via beautifulsoup4
stevedore==5.4.0 # via oslo-config
tabulate==0.9.0 # via -r requirements.in
thesmuggler==1.0.1 # via -r requirements.in
timeago==1.0.16 # via -r requirements.in
typing-extensions==4.12.2 # via typing-inspect
@ -65,8 +48,5 @@ typing-inspect==0.9.0 # via dataclasses-json
tzlocal==5.2 # via -r requirements.in
update-checker==0.18.0 # via -r requirements.in
urllib3==2.2.3 # via requests
validators==0.22.0 # via click-params
werkzeug==3.1.3 # via flask
wrapt==1.17.0 # via -r requirements.in, debtcollector, deprecated
wsproto==1.2.0 # via simple-websocket
wrapt==1.17.0 # via -r requirements.in, debtcollector
zipp==3.21.0 # via importlib-metadata

View File

@ -27,8 +27,8 @@ class TestSendMessageCommand(unittest.TestCase):
if password:
CONF.aprs_network.password = password
CONF.admin.user = "admin"
CONF.admin.password = "password"
# CONF.aprsd_admin_extension.user = "admin"
# CONF.aprsd_admin_extension.password = "password"
@mock.patch("aprsd.log.log.setup_logging")
def test_no_tocallsign(self, mock_logging):

View File

@ -1,90 +0,0 @@
import typing as t
import unittest
from unittest import mock
from click.testing import CliRunner
import flask
import flask_socketio
from oslo_config import cfg
from aprsd import conf # noqa: F401
from aprsd.client import fake as fake_client
from aprsd.cmds import webchat # noqa
from aprsd.packets import core
from .. import fake
CONF = cfg.CONF
F = t.TypeVar("F", bound=t.Callable[..., t.Any])
class TestSendMessageCommand(unittest.TestCase):
def config_and_init(self, login=None, password=None):
CONF.callsign = fake.FAKE_TO_CALLSIGN
CONF.trace_enabled = False
CONF.watch_list.packet_keep_count = 1
if login:
CONF.aprs_network.login = login
if password:
CONF.aprs_network.password = password
CONF.admin.user = "admin"
CONF.admin.password = "password"
@mock.patch("aprsd.log.log.setup_logging")
def test_init_flask(self, mock_logging):
"""Make sure we get an error if there is no login and config."""
CliRunner()
self.config_and_init()
socketio = webchat.init_flask("DEBUG", False)
self.assertIsInstance(socketio, flask_socketio.SocketIO)
self.assertIsInstance(webchat.flask_app, flask.Flask)
@mock.patch("aprsd.packets.tracker.PacketTrack.remove")
@mock.patch("aprsd.cmds.webchat.socketio")
def test_process_ack_packet(
self,
mock_remove, mock_socketio,
):
self.config_and_init()
mock_socketio.emit = mock.MagicMock()
# Create an ACK packet
packet = fake.fake_ack_packet()
mock_queue = mock.MagicMock()
socketio = mock.MagicMock()
wcp = webchat.WebChatProcessPacketThread(mock_queue, socketio)
wcp.process_ack_packet(packet)
mock_remove.called_once()
mock_socketio.called_once()
@mock.patch("aprsd.threads.tx.send")
@mock.patch("aprsd.packets.PacketList.rx")
@mock.patch("aprsd.cmds.webchat.socketio")
@mock.patch("aprsd.client.factory.ClientFactory.create")
def test_process_our_message_packet(
self,
mock_tx_send,
mock_packet_add,
mock_socketio,
mock_factory,
):
self.config_and_init()
mock_socketio.emit = mock.MagicMock()
packet = fake.fake_packet(
message="blah",
msg_number=1,
message_format=core.PACKET_TYPE_MESSAGE,
)
mock_factory.return_value = fake_client.APRSDFakeClient()
mock_queue = mock.MagicMock()
socketio = mock.MagicMock()
wcp = webchat.WebChatProcessPacketThread(mock_queue, socketio)
wcp.process_our_message_packet(packet)
mock_packet_add.called_once()
mock_socketio.called_once()