mirror of
https://github.com/craigerl/aprsd.git
synced 2026-08-16 00:23:34 -04:00
Compare commits
83 Commits
webchat-saved
...
v3.2.3
| Author | SHA1 | Date | |
|---|---|---|---|
| 12a3113192 | |||
| 026a64c003 | |||
| 682e138ec2 | |||
| e4e9c6e98b | |||
| f02824b796 | |||
| 530ac30a09 | |||
| 9350cf6534 | |||
| 651cf014b7 | |||
| b6df9de8aa | |||
| 0fd7daaae0 | |||
| 0433768784 | |||
| a8f73610fe | |||
| c0e2ef1199 | |||
| 809a41f123 | |||
| b0bfdaa1fb | |||
| b73373db3f | |||
| 6b397cbdf1 | |||
| 638128adf8 | |||
| b9dd21bc14 | |||
| fae7032346 | |||
| 4b1214de74 | |||
| 763c9ab897 | |||
| fe1ebf2ec1 | |||
| c01037d398 | |||
| 072a1f4430 | |||
| 8b2613ec47 | |||
| d39ce76475 | |||
| 3e9c3612ba | |||
| 8746a9477c | |||
| 7d0524cee5 | |||
| 5828643f2e | |||
| 313ea5b6a5 | |||
| 7853e19c79 | |||
| acf2b62bce | |||
| 8e9a0213e9 | |||
| bf905a0e9f | |||
| 5ae45ce42f | |||
| 0155923341 | |||
| 156d9d9592 | |||
| 81169600bd | |||
| 746eeb81b0 | |||
| f41488b48a | |||
| 116f201394 | |||
| ddd4d25e9d | |||
| e2f89a6043 | |||
| 544600a96b | |||
| c16f3a0bb2 | |||
| 59cec1317d | |||
| 751bbc2514 | |||
| 9bdfd166fd | |||
| f79b88ec1b | |||
| 99a0f877f4 | |||
| 4f87d5da12 | |||
| 0d7e50d2ba | |||
| 1f6c55d2bf | |||
| 740889426a | |||
| c9dc4f67d4 | |||
| 788a72c643 | |||
| 1e3d0d4faf | |||
| 82d25915fc | |||
| 12dfdefb62 | |||
| d63c6854af | |||
| 6b083d4c4d | |||
| ff358987a9 | |||
| 412ab54303 | |||
| 3f5dbe0a12 | |||
| 9635893934 | |||
| f151ae4348 | |||
| 7130ca2fd9 | |||
| b393060edb | |||
| f770c5ffd5 | |||
| ef206b1283 | |||
| 140fa4ace4 | |||
| 81a19dd101 | |||
| 9985c8bf25 | |||
| 1400e3e711 | |||
| 8a90d5480a | |||
| b4e02c760e | |||
| ba6b410795 | |||
| 70ddc44b5c | |||
| 852760220f | |||
| 14e984c9b4 | |||
| 29f21a9469 |
@@ -1,6 +1,82 @@
|
||||
CHANGES
|
||||
=======
|
||||
|
||||
v3.2.2
|
||||
------
|
||||
|
||||
* Fix for types
|
||||
* Fix wsgi for prod
|
||||
* pep8 fixes
|
||||
* remove python 3.12 from github builds
|
||||
* Fixed datetime access in core.py
|
||||
* removed invalid reference to config.py
|
||||
* Updated requirements
|
||||
* Reworked the admin graphs
|
||||
* Test new packet serialization
|
||||
* Try to localize js libs and css for no internet
|
||||
* Normalize listen --aprs-login
|
||||
* Bump werkzeug from 2.3.7 to 3.0.1
|
||||
* Update INSTALL with new conf files
|
||||
* Bump urllib3 from 2.0.6 to 2.0.7
|
||||
|
||||
v3.2.1
|
||||
------
|
||||
|
||||
* Changelog for 3.2.1
|
||||
* Update index.html disable form autocomplete
|
||||
* Update the packet\_dupe\_timeout warning
|
||||
* Update the webchat paths
|
||||
* Changed the path option to a ListOpt
|
||||
* Fixed default path for tcp\_kiss client
|
||||
* Set a default password for admin
|
||||
* Fix path for KISS clients
|
||||
* Added packet\_dupe\_timeout conf
|
||||
* Add ability to change path on every TX packet
|
||||
* Make Packet objects hashable
|
||||
* Bump urllib3 from 2.0.4 to 2.0.6
|
||||
* Don't process AckPackets as dupes
|
||||
* Fixed another msgNo int issue
|
||||
* Fixed issue with packet tracker and msgNO Counter
|
||||
* Fixed import of Mutablemapping
|
||||
* pep8 fixes
|
||||
* rewrote packet\_list and drop dupe packets
|
||||
* Log a warning on dupe
|
||||
* Fix for dupe packets
|
||||
|
||||
v3.2.0
|
||||
------
|
||||
|
||||
* Update Changelog for 3.2.0
|
||||
* minor cleanup prior to release
|
||||
* Webchat: fix input maxlength
|
||||
* WebChat: cleanup some console.logs
|
||||
* WebChat: flash a dupe message
|
||||
* Webchat: Fix issue accessing msg.id
|
||||
* Webchat: Fix chat css on older browsers
|
||||
* WebChat: new tab should get focus
|
||||
* Bump gevent from 23.9.0.post1 to 23.9.1
|
||||
* Webchat: Fix pep8 errors
|
||||
* Webchat: Added tab notifications and raw packet
|
||||
* WebChat: Prevent sending message without callsign
|
||||
* WebChat: fixed content area scrolling
|
||||
* Webchat: tweaks to UI for expanding chat
|
||||
* Webchat: Fixed bug deleteing first tab
|
||||
* Ensure Keepalive doesn't reset client at startup
|
||||
* Ensure parse\_delta\_str doesn't puke
|
||||
* WebChat: Send GPS Beacon working
|
||||
* webchat: got active tab onclick working
|
||||
* webchat: set to\_call to value of tab when selected
|
||||
* Center the webchat input form
|
||||
* Update index.html to use chat.css
|
||||
* Deleted webchat mobile pages
|
||||
* Added close X on webchat tabs
|
||||
* Reworked webchat with new UI
|
||||
* Updated the webchat UI to look like iMessage
|
||||
* Restore previous conversations in webchat
|
||||
* Remove VIM from Dockerfile
|
||||
* recreate client during reset()
|
||||
* updated github workflows
|
||||
* Updated documentation build
|
||||
* Removed admin\_web.py
|
||||
* Removed some RPC server log noise
|
||||
* Fixed admin page packet date
|
||||
|
||||
+3
-2
@@ -27,9 +27,10 @@ pip install -e .
|
||||
|
||||
# CONFIGURE
|
||||
# Now configure aprsd HERE
|
||||
./aprsd sample-config # generates a config.yml template
|
||||
mkdir -p ~/.config/aprsd
|
||||
./aprsd sample-config > ~/.config/aprsd/aprsd.conf # generates a config template
|
||||
|
||||
vi ~/.config/aprsd/config.yml # copy/edit config here
|
||||
vi ~/.config/aprsd/aprsd.conf # copy/edit config here
|
||||
|
||||
aprsd server
|
||||
|
||||
|
||||
+35
-1
@@ -7,7 +7,7 @@ from aprslib.exceptions import LoginError
|
||||
from oslo_config import cfg
|
||||
|
||||
from aprsd import exception
|
||||
from aprsd.clients import aprsis, kiss
|
||||
from aprsd.clients import aprsis, fake, kiss
|
||||
from aprsd.packets import core, packet_list
|
||||
from aprsd.utils import trace
|
||||
|
||||
@@ -17,6 +17,7 @@ LOG = logging.getLogger("APRSD")
|
||||
TRANSPORT_APRSIS = "aprsis"
|
||||
TRANSPORT_TCPKISS = "tcpkiss"
|
||||
TRANSPORT_SERIALKISS = "serialkiss"
|
||||
TRANSPORT_FAKE = "fake"
|
||||
|
||||
# Main must create this from the ClientFactory
|
||||
# object such that it's populated with the
|
||||
@@ -248,6 +249,35 @@ class KISSClient(Client, metaclass=trace.TraceWrapperMetaclass):
|
||||
return self._client
|
||||
|
||||
|
||||
class APRSDFakeClient(Client, metaclass=trace.TraceWrapperMetaclass):
|
||||
|
||||
@staticmethod
|
||||
def is_enabled():
|
||||
if CONF.fake_client.enabled:
|
||||
return True
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def is_configured():
|
||||
return APRSDFakeClient.is_enabled()
|
||||
|
||||
def is_alive(self):
|
||||
return True
|
||||
|
||||
def setup_connection(self):
|
||||
return fake.APRSDFakeClient()
|
||||
|
||||
@staticmethod
|
||||
def transport():
|
||||
return TRANSPORT_FAKE
|
||||
|
||||
def decode_packet(self, *args, **kwargs):
|
||||
LOG.debug(f"kwargs {kwargs}")
|
||||
pkt = kwargs["packet"]
|
||||
LOG.debug(f"Got an APRS Fake Packet '{pkt}'")
|
||||
return pkt
|
||||
|
||||
|
||||
class ClientFactory:
|
||||
_instance = None
|
||||
|
||||
@@ -270,8 +300,11 @@ class ClientFactory:
|
||||
key = TRANSPORT_APRSIS
|
||||
elif KISSClient.is_enabled():
|
||||
key = KISSClient.transport()
|
||||
elif APRSDFakeClient.is_enabled():
|
||||
key = TRANSPORT_FAKE
|
||||
|
||||
builder = self._builders.get(key)
|
||||
LOG.debug(f"Creating client {key}")
|
||||
if not builder:
|
||||
raise ValueError(key)
|
||||
return builder()
|
||||
@@ -312,3 +345,4 @@ class ClientFactory:
|
||||
factory.register(TRANSPORT_APRSIS, APRSISClient)
|
||||
factory.register(TRANSPORT_TCPKISS, KISSClient)
|
||||
factory.register(TRANSPORT_SERIALKISS, KISSClient)
|
||||
factory.register(TRANSPORT_FAKE, APRSDFakeClient)
|
||||
|
||||
@@ -106,7 +106,7 @@ class Aprsdis(aprslib.IS):
|
||||
aprsd.__version__,
|
||||
)
|
||||
|
||||
self.logger.info("Sending login information")
|
||||
self.logger.debug("Sending login information")
|
||||
|
||||
try:
|
||||
self._sendall(login_str)
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
import logging
|
||||
import threading
|
||||
import time
|
||||
|
||||
import aprslib
|
||||
from oslo_config import cfg
|
||||
import wrapt
|
||||
|
||||
from aprsd import conf # noqa
|
||||
from aprsd.packets import core
|
||||
from aprsd.utils import trace
|
||||
|
||||
|
||||
CONF = cfg.CONF
|
||||
LOG = logging.getLogger("APRSD")
|
||||
|
||||
|
||||
class APRSDFakeClient(metaclass=trace.TraceWrapperMetaclass):
|
||||
'''Fake client for testing.'''
|
||||
|
||||
# flag to tell us to stop
|
||||
thread_stop = False
|
||||
|
||||
lock = threading.Lock()
|
||||
path = []
|
||||
|
||||
def __init__(self):
|
||||
LOG.info("Starting APRSDFakeClient client.")
|
||||
self.path = ["WIDE1-1", "WIDE2-1"]
|
||||
|
||||
def stop(self):
|
||||
self.thread_stop = True
|
||||
LOG.info("Shutdown APRSDFakeClient client.")
|
||||
|
||||
def is_alive(self):
|
||||
"""If the connection is alive or not."""
|
||||
return not self.thread_stop
|
||||
|
||||
@wrapt.synchronized(lock)
|
||||
def send(self, packet: core.Packet):
|
||||
"""Send an APRS Message object."""
|
||||
LOG.info(f"Sending packet: {packet}")
|
||||
payload = None
|
||||
if isinstance(packet, core.Packet):
|
||||
packet.prepare()
|
||||
payload = packet.payload.encode("US-ASCII")
|
||||
if packet.path:
|
||||
packet.path
|
||||
else:
|
||||
self.path
|
||||
else:
|
||||
msg_payload = f"{packet.raw}{{{str(packet.msgNo)}"
|
||||
payload = (
|
||||
":{:<9}:{}".format(
|
||||
packet.to_call,
|
||||
msg_payload,
|
||||
)
|
||||
).encode("US-ASCII")
|
||||
|
||||
LOG.debug(
|
||||
f"FAKE::Send '{payload}' TO '{packet.to_call}' From "
|
||||
f"'{packet.from_call}' with PATH \"{self.path}\"",
|
||||
)
|
||||
|
||||
def consumer(self, callback, blocking=False, immortal=False, raw=False):
|
||||
LOG.debug("Start non blocking FAKE consumer")
|
||||
# Generate packets here?
|
||||
raw = "GTOWN>APDW16,WIDE1-1,WIDE2-1:}KM6LYW-9>APZ100,TCPIP,GTOWN*::KM6LYW :KM6LYW: 19 Miles SW"
|
||||
pkt_raw = aprslib.parse(raw)
|
||||
pkt = core.Packet.factory(pkt_raw)
|
||||
callback(packet=pkt)
|
||||
LOG.debug(f"END blocking FAKE consumer {self}")
|
||||
time.sleep(8)
|
||||
@@ -14,6 +14,8 @@ LOG = logging.getLogger("APRSD")
|
||||
|
||||
|
||||
class KISS3Client:
|
||||
path = []
|
||||
|
||||
def __init__(self):
|
||||
self.setup()
|
||||
|
||||
@@ -34,6 +36,7 @@ class KISS3Client:
|
||||
speed=CONF.kiss_serial.baudrate,
|
||||
strip_df_start=True,
|
||||
)
|
||||
self.path = CONF.kiss_serial.path
|
||||
elif CONF.kiss_tcp.enabled:
|
||||
LOG.debug(
|
||||
"KISS({}) TCP Connection to {}:{}".format(
|
||||
@@ -47,6 +50,7 @@ class KISS3Client:
|
||||
port=CONF.kiss_tcp.port,
|
||||
strip_df_start=True,
|
||||
)
|
||||
self.path = CONF.kiss_tcp.path
|
||||
|
||||
LOG.debug("Starting KISS interface connection")
|
||||
self.kiss.start()
|
||||
@@ -87,10 +91,12 @@ class KISS3Client:
|
||||
"""Send an APRS Message object."""
|
||||
|
||||
payload = None
|
||||
path = ["WIDE1-1", "WIDE2-1"]
|
||||
path = self.path
|
||||
if isinstance(packet, core.Packet):
|
||||
packet.prepare()
|
||||
payload = packet.payload.encode("US-ASCII")
|
||||
if packet.path:
|
||||
path = packet.path
|
||||
else:
|
||||
msg_payload = f"{packet.raw}{{{str(packet.msgNo)}"
|
||||
payload = (
|
||||
|
||||
@@ -144,7 +144,7 @@ def listen(
|
||||
if not aprs_login:
|
||||
click.echo(ctx.get_help())
|
||||
click.echo("")
|
||||
ctx.fail("Must set --aprs_login or APRS_LOGIN")
|
||||
ctx.fail("Must set --aprs-login or APRS_LOGIN")
|
||||
ctx.exit()
|
||||
|
||||
if not aprs_password:
|
||||
|
||||
+38
-70
@@ -15,7 +15,6 @@ from flask.logging import default_handler
|
||||
from flask_httpauth import HTTPBasicAuth
|
||||
from flask_socketio import Namespace, SocketIO
|
||||
from oslo_config import cfg
|
||||
from user_agents import parse as ua_parse
|
||||
from werkzeug.security import check_password_hash, generate_password_hash
|
||||
import wrapt
|
||||
|
||||
@@ -24,7 +23,7 @@ from aprsd import cli_helper, client, conf, packets, stats, threads, utils
|
||||
from aprsd.log import rich as aprsd_logging
|
||||
from aprsd.main import cli
|
||||
from aprsd.threads import rx, tx
|
||||
from aprsd.utils import objectstore, trace
|
||||
from aprsd.utils import trace
|
||||
|
||||
|
||||
CONF = cfg.CONF
|
||||
@@ -58,7 +57,8 @@ def signal_handler(sig, frame):
|
||||
signal.signal(signal.SIGTERM, sys.exit(0))
|
||||
|
||||
|
||||
class SentMessages(objectstore.ObjectStoreMixin):
|
||||
class SentMessages:
|
||||
|
||||
_instance = None
|
||||
lock = threading.Lock()
|
||||
|
||||
@@ -75,25 +75,7 @@ class SentMessages(objectstore.ObjectStoreMixin):
|
||||
|
||||
@wrapt.synchronized(lock)
|
||||
def add(self, msg):
|
||||
self.data[msg.msgNo] = self.create(msg.msgNo)
|
||||
self.data[msg.msgNo]["from"] = msg.from_call
|
||||
self.data[msg.msgNo]["to"] = msg.to_call
|
||||
self.data[msg.msgNo]["message"] = msg.message_text.rstrip("\n")
|
||||
self.data[msg.msgNo]["raw"] = msg.message_text.rstrip("\n")
|
||||
|
||||
def create(self, id):
|
||||
return {
|
||||
"id": id,
|
||||
"ts": time.time(),
|
||||
"ack": False,
|
||||
"from": None,
|
||||
"to": None,
|
||||
"raw": None,
|
||||
"message": None,
|
||||
"status": None,
|
||||
"last_update": None,
|
||||
"reply": None,
|
||||
}
|
||||
self.data[msg.msgNo] = msg.__dict__
|
||||
|
||||
@wrapt.synchronized(lock)
|
||||
def __len__(self):
|
||||
@@ -149,33 +131,17 @@ class WebChatProcessPacketThread(rx.APRSDProcessPacketThread):
|
||||
def process_ack_packet(self, packet: packets.AckPacket):
|
||||
super().process_ack_packet(packet)
|
||||
ack_num = packet.get("msgNo")
|
||||
SentMessages().ack(int(ack_num))
|
||||
SentMessages().ack(ack_num)
|
||||
self.socketio.emit(
|
||||
"ack", SentMessages().get(int(ack_num)),
|
||||
"ack", SentMessages().get(ack_num),
|
||||
namespace="/sendmsg",
|
||||
)
|
||||
self.got_ack = True
|
||||
|
||||
def process_our_message_packet(self, packet: packets.MessagePacket):
|
||||
LOG.info(f"process MessagePacket {repr(packet)}")
|
||||
packet.get("addresse", None)
|
||||
fromcall = packet.from_call
|
||||
|
||||
message = packet.get("message_text", None)
|
||||
msg = {
|
||||
"id": packet.msgNo,
|
||||
"ts": packet.get("timestamp", time.time()),
|
||||
"ack": False,
|
||||
"from": fromcall,
|
||||
"to": packet.to_call,
|
||||
"raw": packet.raw,
|
||||
"message": message,
|
||||
"status": None,
|
||||
"last_update": None,
|
||||
"reply": None,
|
||||
}
|
||||
self.socketio.emit(
|
||||
"new", msg,
|
||||
"new", packet.__dict__,
|
||||
namespace="/sendmsg",
|
||||
)
|
||||
|
||||
@@ -191,25 +157,26 @@ def _get_transport(stats):
|
||||
"APRS-IS Server: <a href='http://status.aprs2.net' >"
|
||||
"{}</a>".format(stats["stats"]["aprs-is"]["server"])
|
||||
)
|
||||
else:
|
||||
# We might be connected to a KISS socket?
|
||||
if client.KISSClient.is_enabled():
|
||||
transport = client.KISSClient.transport()
|
||||
if transport == client.TRANSPORT_TCPKISS:
|
||||
aprs_connection = (
|
||||
"TCPKISS://{}:{}".format(
|
||||
CONF.kiss_tcp.host,
|
||||
CONF.kiss_tcp.port,
|
||||
)
|
||||
)
|
||||
elif transport == client.TRANSPORT_SERIALKISS:
|
||||
# for pep8 violation
|
||||
aprs_connection = (
|
||||
"SerialKISS://{}@{} baud".format(
|
||||
CONF.kiss_serial.device,
|
||||
CONF.kiss_serial.baudrate,
|
||||
),
|
||||
elif client.KISSClient.is_enabled():
|
||||
transport = client.KISSClient.transport()
|
||||
if transport == client.TRANSPORT_TCPKISS:
|
||||
aprs_connection = (
|
||||
"TCPKISS://{}:{}".format(
|
||||
CONF.kiss_tcp.host,
|
||||
CONF.kiss_tcp.port,
|
||||
)
|
||||
)
|
||||
elif transport == client.TRANSPORT_SERIALKISS:
|
||||
# for pep8 violation
|
||||
aprs_connection = (
|
||||
"SerialKISS://{}@{} baud".format(
|
||||
CONF.kiss_serial.device,
|
||||
CONF.kiss_serial.baudrate,
|
||||
),
|
||||
)
|
||||
elif CONF.fake_client.enabled:
|
||||
transport = client.TRANSPORT_FAKE
|
||||
aprs_connection = "Fake Client"
|
||||
|
||||
return transport, aprs_connection
|
||||
|
||||
@@ -217,20 +184,10 @@ def _get_transport(stats):
|
||||
@auth.login_required
|
||||
@flask_app.route("/")
|
||||
def index():
|
||||
ua_str = request.headers.get("User-Agent")
|
||||
# this takes about 2 seconds :(
|
||||
user_agent = ua_parse(ua_str)
|
||||
LOG.debug(f"Is mobile? {user_agent.is_mobile}")
|
||||
stats = _stats()
|
||||
|
||||
if user_agent.is_mobile:
|
||||
html_template = "mobile.html"
|
||||
else:
|
||||
html_template = "index.html"
|
||||
|
||||
# For development
|
||||
# html_template = "mobile.html"
|
||||
|
||||
html_template = "index.html"
|
||||
LOG.debug(f"Template {html_template}")
|
||||
|
||||
transport, aprs_connection = _get_transport(stats)
|
||||
@@ -323,11 +280,22 @@ class SendMessageNamespace(Namespace):
|
||||
LOG.debug(f"WS: on_send {data}")
|
||||
self.request = data
|
||||
data["from"] = CONF.callsign
|
||||
path = data.get("path", None)
|
||||
if not path:
|
||||
path = []
|
||||
elif "," in path:
|
||||
path_opts = path.split(",")
|
||||
path = [x.strip() for x in path_opts]
|
||||
else:
|
||||
path = [path]
|
||||
|
||||
pkt = packets.MessagePacket(
|
||||
from_call=data["from"],
|
||||
to_call=data["to"].upper(),
|
||||
message_text=data["message"],
|
||||
path=path,
|
||||
)
|
||||
pkt.prepare()
|
||||
self.msg = pkt
|
||||
msgs = SentMessages()
|
||||
msgs.add(pkt)
|
||||
|
||||
@@ -11,14 +11,21 @@ aprs_group = cfg.OptGroup(
|
||||
name="aprs_network",
|
||||
title="APRS-IS Network settings",
|
||||
)
|
||||
|
||||
kiss_serial_group = cfg.OptGroup(
|
||||
name="kiss_serial",
|
||||
title="KISS Serial device connection",
|
||||
)
|
||||
|
||||
kiss_tcp_group = cfg.OptGroup(
|
||||
name="kiss_tcp",
|
||||
title="KISS TCP/IP Device connection",
|
||||
)
|
||||
|
||||
fake_client_group = cfg.OptGroup(
|
||||
name="fake_client",
|
||||
title="Fake Client settings",
|
||||
)
|
||||
aprs_opts = [
|
||||
cfg.BoolOpt(
|
||||
"enabled",
|
||||
@@ -65,6 +72,11 @@ kiss_serial_opts = [
|
||||
default=9600,
|
||||
help="The Serial device baud rate for communication",
|
||||
),
|
||||
cfg.ListOpt(
|
||||
"path",
|
||||
default=["WIDE1-1", "WIDE2-1"],
|
||||
help="The APRS path to use for wide area coverage.",
|
||||
),
|
||||
]
|
||||
|
||||
kiss_tcp_opts = [
|
||||
@@ -82,6 +94,19 @@ kiss_tcp_opts = [
|
||||
default=8001,
|
||||
help="The KISS TCP/IP network port",
|
||||
),
|
||||
cfg.ListOpt(
|
||||
"path",
|
||||
default=["WIDE1-1", "WIDE2-1"],
|
||||
help="The APRS path to use for wide area coverage.",
|
||||
),
|
||||
]
|
||||
|
||||
fake_client_opts = [
|
||||
cfg.BoolOpt(
|
||||
"enabled",
|
||||
default=False,
|
||||
help="Enable fake client connection.",
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
@@ -93,10 +118,14 @@ def register_opts(config):
|
||||
config.register_opts(kiss_serial_opts, group=kiss_serial_group)
|
||||
config.register_opts(kiss_tcp_opts, group=kiss_tcp_group)
|
||||
|
||||
config.register_group(fake_client_group)
|
||||
config.register_opts(fake_client_opts, group=fake_client_group)
|
||||
|
||||
|
||||
def list_opts():
|
||||
return {
|
||||
aprs_group.name: aprs_opts,
|
||||
kiss_serial_group.name: kiss_serial_opts,
|
||||
kiss_tcp_group.name: kiss_tcp_opts,
|
||||
fake_client_group.name: fake_client_opts,
|
||||
}
|
||||
|
||||
@@ -65,6 +65,11 @@ aprsd_opts = [
|
||||
"2 means 1 packet every 2 seconds allowed."
|
||||
"5 means 1 pack packet every 5 seconds allowed",
|
||||
),
|
||||
cfg.IntOpt(
|
||||
"packet_dupe_timeout",
|
||||
default=60,
|
||||
help="The number of seconds before a packet is not considered a duplicate.",
|
||||
),
|
||||
]
|
||||
|
||||
watch_list_opts = [
|
||||
@@ -119,6 +124,7 @@ admin_opts = [
|
||||
),
|
||||
cfg.StrOpt(
|
||||
"password",
|
||||
default="password",
|
||||
secret=True,
|
||||
help="Admin interface password",
|
||||
),
|
||||
|
||||
@@ -18,6 +18,11 @@ owm_wx_group = cfg.OptGroup(
|
||||
title="Options for the OWMWeatherPlugin",
|
||||
)
|
||||
|
||||
location_group = cfg.OptGroup(
|
||||
name="location_plugin",
|
||||
title="Options for the LocationPlugin",
|
||||
)
|
||||
|
||||
aprsfi_opts = [
|
||||
cfg.StrOpt(
|
||||
"apiKey",
|
||||
@@ -62,6 +67,106 @@ avwx_opts = [
|
||||
),
|
||||
]
|
||||
|
||||
location_opts = [
|
||||
cfg.StrOpt(
|
||||
"geopy_geocoder",
|
||||
choices=[
|
||||
"ArcGIS", "AzureMaps", "Baidu", "Bing", "GoogleV3", "HERE",
|
||||
"Nominatim", "OpenCage", "TomTom", "USGov", "What3Words", "Woosmap",
|
||||
],
|
||||
default="Nominatim",
|
||||
help="The geopy geocoder to use. Default is Nominatim."
|
||||
"See https://geopy.readthedocs.io/en/stable/#module-geopy.geocoders"
|
||||
"for more information.",
|
||||
),
|
||||
cfg.StrOpt(
|
||||
"user_agent",
|
||||
default="APRSD",
|
||||
help="The user agent to use for the Nominatim geocoder."
|
||||
"See https://geopy.readthedocs.io/en/stable/#module-geopy.geocoders"
|
||||
"for more information.",
|
||||
),
|
||||
cfg.StrOpt(
|
||||
"arcgis_username",
|
||||
default=None,
|
||||
help="The username to use for the ArcGIS geocoder."
|
||||
"See https://geopy.readthedocs.io/en/latest/#arcgis"
|
||||
"for more information."
|
||||
"Only used for the ArcGIS geocoder.",
|
||||
),
|
||||
cfg.StrOpt(
|
||||
"arcgis_password",
|
||||
default=None,
|
||||
help="The password to use for the ArcGIS geocoder."
|
||||
"See https://geopy.readthedocs.io/en/latest/#arcgis"
|
||||
"for more information."
|
||||
"Only used for the ArcGIS geocoder.",
|
||||
),
|
||||
cfg.StrOpt(
|
||||
"azuremaps_subscription_key",
|
||||
help="The subscription key to use for the AzureMaps geocoder."
|
||||
"See https://geopy.readthedocs.io/en/latest/#azuremaps"
|
||||
"for more information."
|
||||
"Only used for the AzureMaps geocoder.",
|
||||
),
|
||||
cfg.StrOpt(
|
||||
"baidu_api_key",
|
||||
help="The API key to use for the Baidu geocoder."
|
||||
"See https://geopy.readthedocs.io/en/latest/#baidu"
|
||||
"for more information."
|
||||
"Only used for the Baidu geocoder.",
|
||||
),
|
||||
cfg.StrOpt(
|
||||
"bing_api_key",
|
||||
help="The API key to use for the Bing geocoder."
|
||||
"See https://geopy.readthedocs.io/en/latest/#bing"
|
||||
"for more information."
|
||||
"Only used for the Bing geocoder.",
|
||||
),
|
||||
cfg.StrOpt(
|
||||
"google_api_key",
|
||||
help="The API key to use for the Google geocoder."
|
||||
"See https://geopy.readthedocs.io/en/latest/#googlev3"
|
||||
"for more information."
|
||||
"Only used for the Google geocoder.",
|
||||
),
|
||||
cfg.StrOpt(
|
||||
"here_api_key",
|
||||
help="The API key to use for the HERE geocoder."
|
||||
"See https://geopy.readthedocs.io/en/latest/#here"
|
||||
"for more information."
|
||||
"Only used for the HERE geocoder.",
|
||||
),
|
||||
cfg.StrOpt(
|
||||
"opencage_api_key",
|
||||
help="The API key to use for the OpenCage geocoder."
|
||||
"See https://geopy.readthedocs.io/en/latest/#opencage"
|
||||
"for more information."
|
||||
"Only used for the OpenCage geocoder.",
|
||||
),
|
||||
cfg.StrOpt(
|
||||
"tomtom_api_key",
|
||||
help="The API key to use for the TomTom geocoder."
|
||||
"See https://geopy.readthedocs.io/en/latest/#tomtom"
|
||||
"for more information."
|
||||
"Only used for the TomTom geocoder.",
|
||||
),
|
||||
cfg.StrOpt(
|
||||
"what3words_api_key",
|
||||
help="The API key to use for the What3Words geocoder."
|
||||
"See https://geopy.readthedocs.io/en/latest/#what3words"
|
||||
"for more information."
|
||||
"Only used for the What3Words geocoder.",
|
||||
),
|
||||
cfg.StrOpt(
|
||||
"woosmap_api_key",
|
||||
help="The API key to use for the Woosmap geocoder."
|
||||
"See https://geopy.readthedocs.io/en/latest/#woosmap"
|
||||
"for more information."
|
||||
"Only used for the Woosmap geocoder.",
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
def register_opts(config):
|
||||
config.register_group(aprsfi_group)
|
||||
@@ -72,6 +177,8 @@ def register_opts(config):
|
||||
config.register_opts(owm_wx_opts, group=owm_wx_group)
|
||||
config.register_group(avwx_group)
|
||||
config.register_opts(avwx_opts, group=avwx_group)
|
||||
config.register_group(location_group)
|
||||
config.register_opts(location_opts, group=location_group)
|
||||
|
||||
|
||||
def list_opts():
|
||||
@@ -80,4 +187,5 @@ def list_opts():
|
||||
query_group.name: query_plugin_opts,
|
||||
owm_wx_group.name: owm_wx_opts,
|
||||
avwx_group.name: avwx_opts,
|
||||
location_group.name: location_opts,
|
||||
}
|
||||
|
||||
@@ -145,7 +145,9 @@ def sample_config(ctx):
|
||||
if not sys.argv[1:]:
|
||||
raise SystemExit
|
||||
raise
|
||||
LOG.warning(conf.namespace)
|
||||
generator.generate(conf)
|
||||
return
|
||||
|
||||
|
||||
@cli.command()
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from aprsd.packets.core import ( # noqa: F401
|
||||
AckPacket, GPSPacket, MessagePacket, MicEPacket, Packet, PathPacket,
|
||||
RejectPacket, StatusPacket, WeatherPacket,
|
||||
AckPacket, GPSPacket, MessagePacket, MicEPacket, Packet, RejectPacket,
|
||||
StatusPacket, WeatherPacket,
|
||||
)
|
||||
from aprsd.packets.packet_list import PacketList # noqa: F401
|
||||
from aprsd.packets.seen_list import SeenList # noqa: F401
|
||||
|
||||
+93
-61
@@ -1,7 +1,6 @@
|
||||
import abc
|
||||
from dataclasses import asdict, dataclass, field
|
||||
import datetime
|
||||
import json
|
||||
from datetime import datetime
|
||||
import logging
|
||||
import re
|
||||
import time
|
||||
@@ -9,9 +8,9 @@ import time
|
||||
from typing import List
|
||||
|
||||
import dacite
|
||||
from dataclasses_json import dataclass_json
|
||||
|
||||
from aprsd.utils import counter
|
||||
from aprsd.utils import json as aprsd_json
|
||||
|
||||
|
||||
LOG = logging.getLogger("APRSD")
|
||||
@@ -28,12 +27,20 @@ PACKET_TYPE_BEACON = "beacon"
|
||||
PACKET_TYPE_THIRDPARTY = "thirdparty"
|
||||
PACKET_TYPE_UNCOMPRESSED = "uncompressed"
|
||||
|
||||
NO_DATE = datetime(1900, 10, 24)
|
||||
|
||||
def _int_timestamp():
|
||||
|
||||
def _init_timestamp():
|
||||
"""Build a unix style timestamp integer"""
|
||||
return int(round(time.time()))
|
||||
|
||||
|
||||
def _init_send_time():
|
||||
# We have to use a datetime here, or the json encoder
|
||||
# Fails on a NoneType.
|
||||
return NO_DATE
|
||||
|
||||
|
||||
def _init_msgNo(): # noqa: N802
|
||||
"""For some reason __post__init doesn't get called.
|
||||
|
||||
@@ -45,42 +52,66 @@ def _init_msgNo(): # noqa: N802
|
||||
return c.value
|
||||
|
||||
|
||||
@dataclass
|
||||
def factory_from_dict(packet_dict):
|
||||
pkt_type = get_packet_type(packet_dict)
|
||||
if pkt_type:
|
||||
cls = TYPE_LOOKUP[pkt_type]
|
||||
return cls.from_dict(packet_dict)
|
||||
|
||||
|
||||
def factory_from_json(packet_dict):
|
||||
pkt_type = get_packet_type(packet_dict)
|
||||
if pkt_type:
|
||||
return TYPE_LOOKUP[pkt_type].from_json(packet_dict)
|
||||
|
||||
|
||||
@dataclass_json
|
||||
@dataclass(unsafe_hash=True)
|
||||
class Packet(metaclass=abc.ABCMeta):
|
||||
from_call: str
|
||||
to_call: str
|
||||
addresse: str = None
|
||||
format: str = None
|
||||
from_call: str = field(default=None)
|
||||
to_call: str = field(default=None)
|
||||
addresse: str = field(default=None)
|
||||
format: str = field(default=None)
|
||||
msgNo: str = field(default_factory=_init_msgNo) # noqa: N815
|
||||
packet_type: str = None
|
||||
timestamp: float = field(default_factory=_int_timestamp)
|
||||
packet_type: str = field(default=None)
|
||||
timestamp: float = field(default_factory=_init_timestamp, compare=False, hash=False)
|
||||
# Holds the raw text string to be sent over the wire
|
||||
# or holds the raw string from input packet
|
||||
raw: str = None
|
||||
raw_dict: dict = field(repr=False, default_factory=lambda: {})
|
||||
raw: str = field(default=None, compare=False, hash=False)
|
||||
raw_dict: dict = field(repr=False, default_factory=lambda: {}, compare=False, hash=False)
|
||||
# Built by calling prepare(). raw needs this built first.
|
||||
payload: str = None
|
||||
payload: str = field(default=None)
|
||||
|
||||
# Fields related to sending packets out
|
||||
send_count: int = field(repr=False, default=0)
|
||||
retry_count: int = field(repr=False, default=3)
|
||||
last_send_time: datetime.timedelta = field(repr=False, default=None)
|
||||
send_count: int = field(repr=False, default=0, compare=False, hash=False)
|
||||
retry_count: int = field(repr=False, default=3, compare=False, hash=False)
|
||||
# last_send_time: datetime = field(
|
||||
# metadata=dc_json_config(
|
||||
# encoder=datetime.isoformat,
|
||||
# decoder=datetime.fromisoformat,
|
||||
# ),
|
||||
# repr=True,
|
||||
# default_factory=_init_send_time,
|
||||
# compare=False,
|
||||
# hash=False
|
||||
# )
|
||||
last_send_time: float = field(repr=False, default=0, compare=False, hash=False)
|
||||
last_send_attempt: int = field(repr=False, default=0, compare=False, hash=False)
|
||||
|
||||
# Do we allow this packet to be saved to send later?
|
||||
allow_delay: bool = field(repr=False, default=True)
|
||||
allow_delay: bool = field(repr=False, default=True, compare=False, hash=False)
|
||||
path: List[str] = field(default_factory=list, compare=False, hash=False)
|
||||
via: str = field(default=None, compare=False, hash=False)
|
||||
|
||||
def __post__init__(self):
|
||||
LOG.warning(f"POST INIT {self}")
|
||||
|
||||
@property
|
||||
def __dict__(self):
|
||||
return asdict(self)
|
||||
|
||||
@property
|
||||
def json(self):
|
||||
"""
|
||||
get the json formated string
|
||||
"""
|
||||
return json.dumps(self.__dict__, cls=aprsd_json.EnhancedJSONEncoder)
|
||||
return self.to_json()
|
||||
|
||||
def get(self, key, default=None):
|
||||
"""Emulate a getter on a dict."""
|
||||
@@ -89,8 +120,13 @@ class Packet(metaclass=abc.ABCMeta):
|
||||
else:
|
||||
return default
|
||||
|
||||
@property
|
||||
def key(self):
|
||||
"""Build a key for finding this packet in a dict."""
|
||||
return f"{self.from_call}:{self.addresse}:{self.msgNo}"
|
||||
|
||||
def update_timestamp(self):
|
||||
self.timestamp = _int_timestamp()
|
||||
self.timestamp = _init_timestamp()
|
||||
|
||||
def prepare(self):
|
||||
"""Do stuff here that is needed prior to sending over the air."""
|
||||
@@ -258,18 +294,9 @@ class Packet(metaclass=abc.ABCMeta):
|
||||
return repr
|
||||
|
||||
|
||||
@dataclass
|
||||
class PathPacket(Packet):
|
||||
path: List[str] = field(default_factory=list)
|
||||
via: str = None
|
||||
|
||||
def _build_payload(self):
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
@dataclass
|
||||
class AckPacket(PathPacket):
|
||||
response: str = None
|
||||
@dataclass(unsafe_hash=True)
|
||||
class AckPacket(Packet):
|
||||
response: str = field(default=None)
|
||||
|
||||
def __post__init__(self):
|
||||
if self.response:
|
||||
@@ -279,9 +306,9 @@ class AckPacket(PathPacket):
|
||||
self.payload = f":{self.to_call.ljust(9)}:ack{self.msgNo}"
|
||||
|
||||
|
||||
@dataclass
|
||||
class RejectPacket(PathPacket):
|
||||
response: str = None
|
||||
@dataclass(unsafe_hash=True)
|
||||
class RejectPacket(Packet):
|
||||
response: str = field(default=None)
|
||||
|
||||
def __post__init__(self):
|
||||
if self.response:
|
||||
@@ -291,9 +318,10 @@ class RejectPacket(PathPacket):
|
||||
self.payload = f":{self.to_call.ljust(9)} :rej{self.msgNo}"
|
||||
|
||||
|
||||
@dataclass
|
||||
class MessagePacket(PathPacket):
|
||||
message_text: str = None
|
||||
@dataclass_json
|
||||
@dataclass(unsafe_hash=True)
|
||||
class MessagePacket(Packet):
|
||||
message_text: str = field(default=None)
|
||||
|
||||
def _filter_for_send(self) -> str:
|
||||
"""Filter and format message string for FCC."""
|
||||
@@ -313,24 +341,24 @@ class MessagePacket(PathPacket):
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class StatusPacket(PathPacket):
|
||||
status: str = None
|
||||
messagecapable: bool = False
|
||||
comment: str = None
|
||||
@dataclass(unsafe_hash=True)
|
||||
class StatusPacket(Packet):
|
||||
status: str = field(default=None)
|
||||
messagecapable: bool = field(default=False)
|
||||
comment: str = field(default=None)
|
||||
|
||||
def _build_payload(self):
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
@dataclass
|
||||
class GPSPacket(PathPacket):
|
||||
latitude: float = 0.00
|
||||
longitude: float = 0.00
|
||||
altitude: float = 0.00
|
||||
rng: float = 0.00
|
||||
posambiguity: int = 0
|
||||
comment: str = None
|
||||
@dataclass(unsafe_hash=True)
|
||||
class GPSPacket(Packet):
|
||||
latitude: float = field(default=0.00)
|
||||
longitude: float = field(default=0.00)
|
||||
altitude: float = field(default=0.00)
|
||||
rng: float = field(default=0.00)
|
||||
posambiguity: int = field(default=0)
|
||||
comment: str = field(default=None)
|
||||
symbol: str = field(default="l")
|
||||
symbol_table: str = field(default="/")
|
||||
|
||||
@@ -408,12 +436,12 @@ class GPSPacket(PathPacket):
|
||||
def _build_time_zulu(self):
|
||||
"""Build the timestamp in UTC/zulu."""
|
||||
if self.timestamp:
|
||||
local_dt = datetime.datetime.fromtimestamp(self.timestamp)
|
||||
local_dt = datetime.fromtimestamp(self.timestamp)
|
||||
else:
|
||||
local_dt = datetime.datetime.now()
|
||||
self.timestamp = datetime.datetime.timestamp(local_dt)
|
||||
local_dt = datetime.now()
|
||||
self.timestamp = datetime.timestamp(local_dt)
|
||||
|
||||
utc_offset_timedelta = datetime.datetime.utcnow() - local_dt
|
||||
utc_offset_timedelta = datetime.utcnow() - local_dt
|
||||
result_utc_datetime = local_dt + utc_offset_timedelta
|
||||
time_zulu = result_utc_datetime.strftime("%d%H%M")
|
||||
return time_zulu
|
||||
@@ -569,7 +597,7 @@ class WeatherPacket(GPSPacket):
|
||||
|
||||
class ThirdParty(Packet):
|
||||
# Holds the encapsulated packet
|
||||
subpacket: Packet = None
|
||||
subpacket: Packet = field(default=None, compare=True, hash=False)
|
||||
|
||||
def __repr__(self):
|
||||
"""Build the repr version of the packet."""
|
||||
@@ -602,7 +630,7 @@ def get_packet_type(packet: dict):
|
||||
|
||||
pkt_format = packet.get("format", None)
|
||||
msg_response = packet.get("response", None)
|
||||
packet_type = "unknown"
|
||||
packet_type = PACKET_TYPE_UNKNOWN
|
||||
if pkt_format == "message" and msg_response == "ack":
|
||||
packet_type = PACKET_TYPE_ACK
|
||||
elif pkt_format == "message" and msg_response == "rej":
|
||||
@@ -622,6 +650,10 @@ def get_packet_type(packet: dict):
|
||||
packet_type = PACKET_TYPE_WX
|
||||
elif pkt_format == PACKET_TYPE_THIRDPARTY:
|
||||
packet_type = PACKET_TYPE_THIRDPARTY
|
||||
|
||||
if packet_type == PACKET_TYPE_UNKNOWN:
|
||||
if "latitude" in packet:
|
||||
packet_type = PACKET_TYPE_BEACON
|
||||
return packet_type
|
||||
|
||||
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
from collections import OrderedDict
|
||||
from collections.abc import MutableMapping
|
||||
import logging
|
||||
import threading
|
||||
|
||||
from oslo_config import cfg
|
||||
import wrapt
|
||||
|
||||
from aprsd import stats, utils
|
||||
from aprsd import stats
|
||||
from aprsd.packets import seen_list
|
||||
|
||||
|
||||
@@ -12,31 +14,29 @@ CONF = cfg.CONF
|
||||
LOG = logging.getLogger("APRSD")
|
||||
|
||||
|
||||
class PacketList:
|
||||
"""Class to track all of the packets rx'd and tx'd by aprsd."""
|
||||
|
||||
class PacketList(MutableMapping):
|
||||
_instance = None
|
||||
lock = threading.Lock()
|
||||
|
||||
packet_list: utils.RingBuffer = utils.RingBuffer(1000)
|
||||
|
||||
_total_rx: int = 0
|
||||
_total_tx: int = 0
|
||||
types = {}
|
||||
|
||||
def __new__(cls, *args, **kwargs):
|
||||
if cls._instance is None:
|
||||
cls._instance = super().__new__(cls)
|
||||
cls._maxlen = 100
|
||||
cls.d = OrderedDict()
|
||||
return cls._instance
|
||||
|
||||
@wrapt.synchronized(lock)
|
||||
def __iter__(self):
|
||||
return iter(self.packet_list)
|
||||
|
||||
@wrapt.synchronized(lock)
|
||||
def rx(self, packet):
|
||||
"""Add a packet that was received."""
|
||||
self._total_rx += 1
|
||||
self.packet_list.append(packet)
|
||||
self._add(packet)
|
||||
ptype = packet.__class__.__name__
|
||||
if not ptype in self.types:
|
||||
self.types[ptype] = {"tx": 0, "rx": 0}
|
||||
self.types[ptype]["rx"] += 1
|
||||
seen_list.SeenList().update_seen(packet)
|
||||
stats.APRSDStats().rx(packet)
|
||||
|
||||
@@ -44,13 +44,51 @@ class PacketList:
|
||||
def tx(self, packet):
|
||||
"""Add a packet that was received."""
|
||||
self._total_tx += 1
|
||||
self.packet_list.append(packet)
|
||||
self._add(packet)
|
||||
ptype = packet.__class__.__name__
|
||||
if not ptype in self.types:
|
||||
self.types[ptype] = {"tx": 0, "rx": 0}
|
||||
self.types[ptype]["tx"] += 1
|
||||
seen_list.SeenList().update_seen(packet)
|
||||
stats.APRSDStats().tx(packet)
|
||||
|
||||
@wrapt.synchronized(lock)
|
||||
def get(self):
|
||||
return self.packet_list.get()
|
||||
def add(self, packet):
|
||||
self._add(packet)
|
||||
|
||||
def _add(self, packet):
|
||||
self[packet.key] = packet
|
||||
|
||||
def copy(self):
|
||||
return self.d.copy()
|
||||
|
||||
@property
|
||||
def maxlen(self):
|
||||
return self._maxlen
|
||||
|
||||
@wrapt.synchronized(lock)
|
||||
def find(self, packet):
|
||||
return self.get(packet.key)
|
||||
|
||||
def __getitem__(self, key):
|
||||
# self.d.move_to_end(key)
|
||||
return self.d[key]
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
if key in self.d:
|
||||
self.d.move_to_end(key)
|
||||
elif len(self.d) == self.maxlen:
|
||||
self.d.popitem(last=False)
|
||||
self.d[key] = value
|
||||
|
||||
def __delitem__(self, key):
|
||||
del self.d[key]
|
||||
|
||||
def __iter__(self):
|
||||
return self.d.__iter__()
|
||||
|
||||
def __len__(self):
|
||||
return len(self.d)
|
||||
|
||||
@wrapt.synchronized(lock)
|
||||
def total_rx(self):
|
||||
|
||||
@@ -62,36 +62,29 @@ class PacketTrack(objectstore.ObjectStoreMixin):
|
||||
def __len__(self):
|
||||
return len(self.data)
|
||||
|
||||
@wrapt.synchronized(lock)
|
||||
def __str__(self):
|
||||
result = "{"
|
||||
for key in self.data.keys():
|
||||
result += f"{key}: {str(self.data[key])}, "
|
||||
result += "}"
|
||||
return result
|
||||
|
||||
@wrapt.synchronized(lock)
|
||||
def add(self, packet):
|
||||
key = int(packet.msgNo)
|
||||
key = packet.msgNo
|
||||
packet._last_send_attempt = 0
|
||||
self.data[key] = packet
|
||||
self.total_tracked += 1
|
||||
|
||||
@wrapt.synchronized(lock)
|
||||
def get(self, id):
|
||||
if id in self.data:
|
||||
return self.data[id]
|
||||
def get(self, key):
|
||||
return self.data.get(key, None)
|
||||
|
||||
@wrapt.synchronized(lock)
|
||||
def remove(self, id):
|
||||
key = int(id)
|
||||
if key in self.data.keys():
|
||||
def remove(self, key):
|
||||
try:
|
||||
del self.data[key]
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
def restart(self):
|
||||
"""Walk the list of messages and restart them if any."""
|
||||
for key in self.data.keys():
|
||||
pkt = self.data[key]
|
||||
if pkt.last_send_attempt < pkt.retry_count:
|
||||
if pkt._last_send_attempt < pkt.retry_count:
|
||||
tx.send(pkt)
|
||||
|
||||
def _resend(self, packet):
|
||||
|
||||
@@ -76,6 +76,7 @@ def fetch_openweathermap(api_key, lat, lon, units="metric", exclude=None):
|
||||
exclude,
|
||||
)
|
||||
)
|
||||
LOG.debug(f"Fetching OWM weather '{url}'")
|
||||
response = requests.get(url)
|
||||
except Exception as e:
|
||||
LOG.error(e)
|
||||
|
||||
@@ -8,6 +8,8 @@ from aprsd.utils import trace
|
||||
|
||||
LOG = logging.getLogger("APRSD")
|
||||
|
||||
DEFAULT_FORTUNE_PATH = '/usr/games/fortune'
|
||||
|
||||
|
||||
class FortunePlugin(plugin.APRSDRegexCommandPluginBase):
|
||||
"""Fortune."""
|
||||
@@ -19,7 +21,8 @@ class FortunePlugin(plugin.APRSDRegexCommandPluginBase):
|
||||
fortune_path = None
|
||||
|
||||
def setup(self):
|
||||
self.fortune_path = shutil.which("fortune")
|
||||
self.fortune_path = shutil.which(DEFAULT_FORTUNE_PATH)
|
||||
LOG.info(f"Fortune path {self.fortune_path}")
|
||||
if not self.fortune_path:
|
||||
self.enabled = False
|
||||
else:
|
||||
|
||||
@@ -2,7 +2,8 @@ import logging
|
||||
import re
|
||||
import time
|
||||
|
||||
from geopy.geocoders import Nominatim
|
||||
from geopy.geocoders import ArcGIS, AzureMaps, Baidu, Bing, GoogleV3
|
||||
from geopy.geocoders import HereV7, Nominatim, OpenCage, TomTom, What3WordsV3, Woosmap
|
||||
from oslo_config import cfg
|
||||
|
||||
from aprsd import packets, plugin, plugin_utils
|
||||
@@ -13,6 +14,82 @@ CONF = cfg.CONF
|
||||
LOG = logging.getLogger("APRSD")
|
||||
|
||||
|
||||
class UsLocation:
|
||||
raw = {}
|
||||
|
||||
def __init__(self, info):
|
||||
self.info = info
|
||||
|
||||
def __str__(self):
|
||||
return self.info
|
||||
|
||||
|
||||
class USGov:
|
||||
"""US Government geocoder that uses the geopy API.
|
||||
|
||||
This is a dummy class the implements the geopy reverse API,
|
||||
so the factory can return an object that conforms to the API.
|
||||
"""
|
||||
def reverse(self, coordinates):
|
||||
"""Reverse geocode a coordinate."""
|
||||
LOG.info(f"USGov reverse geocode {coordinates}")
|
||||
coords = coordinates.split(",")
|
||||
lat = float(coords[0])
|
||||
lon = float(coords[1])
|
||||
result = plugin_utils.get_weather_gov_for_gps(lat, lon)
|
||||
# LOG.info(f"WEATHER: {result}")
|
||||
# LOG.info(f"area description {result['location']['areaDescription']}")
|
||||
if 'location' in result:
|
||||
loc = UsLocation(result['location']['areaDescription'])
|
||||
else:
|
||||
loc = UsLocation("Unknown Location")
|
||||
|
||||
LOG.info(f"USGov reverse geocode LOC {loc}")
|
||||
return loc
|
||||
|
||||
|
||||
def geopy_factory():
|
||||
"""Factory function for geopy geocoders."""
|
||||
geocoder = CONF.location_plugin.geopy_geocoder
|
||||
LOG.info(f"Using geocoder: {geocoder}")
|
||||
user_agent = CONF.location_plugin.user_agent
|
||||
LOG.info(f"Using user_agent: {user_agent}")
|
||||
|
||||
if geocoder == "Nominatim":
|
||||
return Nominatim(user_agent=user_agent)
|
||||
elif geocoder == "USGov":
|
||||
return USGov()
|
||||
elif geocoder == "ArcGIS":
|
||||
return ArcGIS(
|
||||
username=CONF.location_plugin.arcgis_username,
|
||||
password=CONF.location_plugin.arcgis_password,
|
||||
user_agent=user_agent,
|
||||
)
|
||||
elif geocoder == "AzureMaps":
|
||||
return AzureMaps(
|
||||
user_agent=user_agent,
|
||||
subscription_key=CONF.location_plugin.azuremaps_subscription_key,
|
||||
)
|
||||
elif geocoder == "Baidu":
|
||||
return Baidu(user_agent=user_agent, api_key=CONF.location_plugin.baidu_api_key)
|
||||
elif geocoder == "Bing":
|
||||
return Bing(user_agent=user_agent, api_key=CONF.location_plugin.bing_api_key)
|
||||
elif geocoder == "GoogleV3":
|
||||
return GoogleV3(user_agent=user_agent, api_key=CONF.location_plugin.google_api_key)
|
||||
elif geocoder == "HERE":
|
||||
return HereV7(user_agent=user_agent, api_key=CONF.location_plugin.here_api_key)
|
||||
elif geocoder == "OpenCage":
|
||||
return OpenCage(user_agent=user_agent, api_key=CONF.location_plugin.opencage_api_key)
|
||||
elif geocoder == "TomTom":
|
||||
return TomTom(user_agent=user_agent, api_key=CONF.location_plugin.tomtom_api_key)
|
||||
elif geocoder == "What3Words":
|
||||
return What3WordsV3(user_agent=user_agent, api_key=CONF.location_plugin.what3words_api_key)
|
||||
elif geocoder == "Woosmap":
|
||||
return Woosmap(user_agent=user_agent, api_key=CONF.location_plugin.woosmap_api_key)
|
||||
else:
|
||||
raise ValueError(f"Unknown geocoder: {geocoder}")
|
||||
|
||||
|
||||
class LocationPlugin(plugin.APRSDRegexCommandPluginBase, plugin.APRSFIKEYMixin):
|
||||
"""Location!"""
|
||||
|
||||
@@ -57,19 +134,24 @@ class LocationPlugin(plugin.APRSDRegexCommandPluginBase, plugin.APRSFIKEYMixin):
|
||||
# Get some information about their location
|
||||
try:
|
||||
tic = time.perf_counter()
|
||||
geolocator = Nominatim(user_agent="APRSD")
|
||||
geolocator = geopy_factory()
|
||||
LOG.info(f"Using GEOLOCATOR: {geolocator}")
|
||||
coordinates = f"{lat:0.6f}, {lon:0.6f}"
|
||||
location = geolocator.reverse(coordinates)
|
||||
address = location.raw.get("address")
|
||||
LOG.debug(f"GEOLOCATOR address: {address}")
|
||||
toc = time.perf_counter()
|
||||
if address:
|
||||
LOG.info(f"Geopy address {address} took {toc - tic:0.4f}")
|
||||
if address.get("country_code") == "us":
|
||||
area_info = f"{address.get('county')}, {address.get('state')}"
|
||||
if address.get("country_code") == "us":
|
||||
area_info = f"{address.get('county')}, {address.get('state')}"
|
||||
else:
|
||||
# what to do for address for non US?
|
||||
area_info = f"{address.get('country'), 'Unknown'}"
|
||||
else:
|
||||
# what to do for address for non US?
|
||||
area_info = f"{address.get('country'), 'Unknown'}"
|
||||
area_info = str(location)
|
||||
except Exception as ex:
|
||||
LOG.error(ex)
|
||||
LOG.error(f"Failed to fetch Geopy address {ex}")
|
||||
area_info = "Unknown Location"
|
||||
|
||||
|
||||
@@ -26,7 +26,9 @@ class USWeatherPlugin(plugin.APRSDRegexCommandPluginBase, plugin.APRSFIKEYMixin)
|
||||
"weather" - returns weather near the calling callsign
|
||||
"""
|
||||
|
||||
command_regex = r"^([w][x]|[w][x]\s|weather)"
|
||||
# command_regex = r"^([w][x]|[w][x]\s|weather)"
|
||||
command_regex = r"^[wW]"
|
||||
|
||||
command_name = "USWeather"
|
||||
short_description = "Provide USA only weather of GPS Beacon location"
|
||||
|
||||
@@ -189,7 +191,9 @@ class OWMWeatherPlugin(plugin.APRSDRegexCommandPluginBase):
|
||||
|
||||
"""
|
||||
|
||||
command_regex = r"^([w][x]|[w][x]\s|weather)"
|
||||
# command_regex = r"^([w][x]|[w][x]\s|weather)"
|
||||
command_regex = r"^[wW]"
|
||||
|
||||
command_name = "OpenWeatherMap"
|
||||
short_description = "OpenWeatherMap weather of GPS Beacon location"
|
||||
|
||||
@@ -211,7 +215,7 @@ class OWMWeatherPlugin(plugin.APRSDRegexCommandPluginBase):
|
||||
|
||||
@trace.trace
|
||||
def process(self, packet):
|
||||
fromcall = packet.get("from")
|
||||
fromcall = packet.get("from_call")
|
||||
message = packet.get("message_text", None)
|
||||
# ack = packet.get("msgNo", "0")
|
||||
LOG.info(f"OWMWeather Plugin '{message}'")
|
||||
|
||||
@@ -82,7 +82,12 @@ class KeepAliveThread(APRSDThread):
|
||||
|
||||
# check the APRS connection
|
||||
cl = client.factory.create()
|
||||
if not cl.is_alive():
|
||||
# Reset the connection if it's dead and this isn't our
|
||||
# First time through the loop.
|
||||
# The first time through the loop can happen at startup where
|
||||
# The keepalive thread starts before the client has a chance
|
||||
# to make it's connection the first time.
|
||||
if not cl.is_alive() and self.cntr > 0:
|
||||
LOG.error(f"{cl.__class__.__name__} is not alive!!! Resetting")
|
||||
client.factory.create().reset()
|
||||
else:
|
||||
|
||||
+47
-7
@@ -67,11 +67,55 @@ class APRSDPluginRXThread(APRSDRXThread):
|
||||
"""
|
||||
|
||||
def process_packet(self, *args, **kwargs):
|
||||
"""This handles the processing of an inbound packet.
|
||||
|
||||
When a packet is received by the connected client object,
|
||||
it sends the raw packet into this function. This function then
|
||||
decodes the packet via the client, and then processes the packet.
|
||||
Ack Packets are sent to the PluginProcessPacketThread for processing.
|
||||
All other packets have to be checked as a dupe, and then only after
|
||||
we haven't seen this packet before, do we send it to the
|
||||
PluginProcessPacketThread for processing.
|
||||
"""
|
||||
packet = self._client.decode_packet(*args, **kwargs)
|
||||
# LOG.debug(raw)
|
||||
packet.log(header="RX")
|
||||
packets.PacketList().rx(packet)
|
||||
self.packet_queue.put(packet)
|
||||
|
||||
if isinstance(packet, packets.AckPacket):
|
||||
# We don't need to drop AckPackets, those should be
|
||||
# processed.
|
||||
self.packet_queue.put(packet)
|
||||
else:
|
||||
# Make sure we aren't re-processing the same packet
|
||||
# For RF based APRS Clients we can get duplicate packets
|
||||
# So we need to track them and not process the dupes.
|
||||
found = False
|
||||
pkt_list = packets.PacketList()
|
||||
try:
|
||||
# Find the packet in the list of already seen packets
|
||||
# Based on the packet.key
|
||||
found = pkt_list.find(packet)
|
||||
except KeyError:
|
||||
found = False
|
||||
|
||||
if not found:
|
||||
# If we are in the process of already ack'ing
|
||||
# a packet, we should drop the packet
|
||||
# because it's a dupe within the time that
|
||||
# we send the 3 acks for the packet.
|
||||
pkt_list.rx(packet)
|
||||
self.packet_queue.put(packet)
|
||||
elif packet.timestamp - found.timestamp < CONF.packet_dupe_timeout:
|
||||
# If the packet came in within 60 seconds of the
|
||||
# Last time seeing the packet, then we drop it as a dupe.
|
||||
LOG.warning(f"Packet {packet.from_call}:{packet.msgNo} already tracked, dropping.")
|
||||
else:
|
||||
LOG.warning(
|
||||
f"Packet {packet.from_call}:{packet.msgNo} already tracked "
|
||||
f"but older than {CONF.packet_dupe_timeout} seconds. processing.",
|
||||
)
|
||||
pkt_list.rx(packet)
|
||||
self.packet_queue.put(packet)
|
||||
|
||||
|
||||
class APRSDProcessPacketThread(APRSDThread):
|
||||
@@ -125,8 +169,6 @@ class APRSDProcessPacketThread(APRSDThread):
|
||||
|
||||
# We don't put ack packets destined for us through the
|
||||
# plugins.
|
||||
wl = packets.WatchList()
|
||||
wl.update_seen(packet)
|
||||
if (
|
||||
isinstance(packet, packets.AckPacket)
|
||||
and packet.addresse.lower() == our_call
|
||||
@@ -214,9 +256,7 @@ class APRSDPluginProcessPacketThread(APRSDProcessPacketThread):
|
||||
to_call = packet.addresse
|
||||
else:
|
||||
to_call = None
|
||||
# msg = packet.get("message_text", None)
|
||||
# packet.get("msgNo", "0")
|
||||
# packet.get("response", None)
|
||||
|
||||
pm = plugin.PluginManager()
|
||||
try:
|
||||
results = pm.run(packet)
|
||||
|
||||
+6
-7
@@ -1,4 +1,3 @@
|
||||
import datetime
|
||||
import logging
|
||||
import time
|
||||
|
||||
@@ -128,10 +127,10 @@ class SendPacketThread(aprsd_threads.APRSDThread):
|
||||
# Message is still outstanding and needs to be acked.
|
||||
if packet.last_send_time:
|
||||
# Message has a last send time tracking
|
||||
now = datetime.datetime.now()
|
||||
now = int(round(time.time()))
|
||||
sleeptime = (packet.send_count + 1) * 31
|
||||
delta = now - packet.last_send_time
|
||||
if delta > datetime.timedelta(seconds=sleeptime):
|
||||
if delta > sleeptime:
|
||||
# It's time to try to send it again
|
||||
send_now = True
|
||||
else:
|
||||
@@ -140,7 +139,7 @@ class SendPacketThread(aprsd_threads.APRSDThread):
|
||||
if send_now:
|
||||
# no attempt time, so lets send it, and start
|
||||
# tracking the time.
|
||||
packet.last_send_time = datetime.datetime.now()
|
||||
packet.last_send_time = int(round(time.time()))
|
||||
send(packet, direct=True)
|
||||
packet.send_count += 1
|
||||
|
||||
@@ -173,13 +172,13 @@ class SendAckThread(aprsd_threads.APRSDThread):
|
||||
|
||||
if self.packet.last_send_time:
|
||||
# Message has a last send time tracking
|
||||
now = datetime.datetime.now()
|
||||
now = int(round(time.time()))
|
||||
|
||||
# aprs duplicate detection is 30 secs?
|
||||
# (21 only sends first, 28 skips middle)
|
||||
sleep_time = 31
|
||||
delta = now - self.packet.last_send_time
|
||||
if delta > datetime.timedelta(seconds=sleep_time):
|
||||
if delta > sleep_time:
|
||||
# It's time to try to send it again
|
||||
send_now = True
|
||||
elif self.loop_count % 10 == 0:
|
||||
@@ -190,7 +189,7 @@ class SendAckThread(aprsd_threads.APRSDThread):
|
||||
if send_now:
|
||||
send(self.packet, direct=True)
|
||||
self.packet.send_count += 1
|
||||
self.packet.last_send_time = datetime.datetime.now()
|
||||
self.packet.last_send_time = int(round(time.time()))
|
||||
|
||||
time.sleep(1)
|
||||
self.loop_count += 1
|
||||
|
||||
@@ -126,4 +126,8 @@ def parse_delta_str(s):
|
||||
)
|
||||
else:
|
||||
m = re.match(r"(?P<hours>\d+):(?P<minutes>\d+):(?P<seconds>\d[\.\d+]*)", s)
|
||||
return {key: float(val) for key, val in m.groupdict().items()}
|
||||
|
||||
if m:
|
||||
return {key: float(val) for key, val in m.groupdict().items()}
|
||||
else:
|
||||
return {}
|
||||
|
||||
@@ -37,7 +37,7 @@ class PacketCounter:
|
||||
@property
|
||||
@wrapt.synchronized(lock)
|
||||
def value(self):
|
||||
return self.val.value
|
||||
return str(self.val.value)
|
||||
|
||||
@wrapt.synchronized(lock)
|
||||
def __repr__(self):
|
||||
|
||||
@@ -0,0 +1,403 @@
|
||||
var packet_list = {};
|
||||
|
||||
var tx_data = [];
|
||||
var rx_data = [];
|
||||
|
||||
var packet_types_data = {};
|
||||
|
||||
var mem_current = []
|
||||
var mem_peak = []
|
||||
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
|
||||
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 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
|
||||
}
|
||||
console.log(option)
|
||||
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 updateMessagesChart() {
|
||||
updateTypeChart(message_chart, "MessagePacket")
|
||||
}
|
||||
|
||||
function updateAcksChart() {
|
||||
updateTypeChart(ack_chart, "AckPacket")
|
||||
}
|
||||
|
||||
function update_stats( data ) {
|
||||
console.log(data);
|
||||
our_callsign = data["stats"]["aprsd"]["callsign"];
|
||||
$("#version").text( data["stats"]["aprsd"]["version"] );
|
||||
$("#aprs_connection").html( data["aprs_connection"] );
|
||||
$("#uptime").text( "uptime: " + data["stats"]["aprsd"]["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, data["stats"]["packets"]["sent"], data["stats"]["packets"]["received"]);
|
||||
updatePacketTypesData(ts, data["stats"]["packets"]["types"]);
|
||||
updatePacketTypesChart();
|
||||
updateMessagesChart();
|
||||
updateAcksChart();
|
||||
updateMemChart(ts, data["stats"]["aprsd"]["memory_current"], data["stats"]["aprsd"]["memory_peak"]);
|
||||
//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"]);
|
||||
}
|
||||
@@ -6,6 +6,7 @@
|
||||
<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>
|
||||
@@ -15,7 +16,7 @@
|
||||
<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/charts.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>
|
||||
@@ -83,6 +84,7 @@
|
||||
<div class="item" data-tab="plugin-tab">Plugins</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>
|
||||
|
||||
@@ -92,25 +94,25 @@
|
||||
<div class="ui equal width relaxed grid">
|
||||
<div class="row">
|
||||
<div class="column">
|
||||
<div class="ui segment" style="height: 300px">
|
||||
<canvas id="packetsChart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column">
|
||||
<div class="ui segment" style="height: 300px">
|
||||
<canvas id="messageChart"></canvas>
|
||||
</div>
|
||||
<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">
|
||||
<canvas id="emailChart"></canvas>
|
||||
</div>
|
||||
<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="messagesChart"></div>
|
||||
</div>
|
||||
<div class="column">
|
||||
<div class="ui segment" style="height: 300px">
|
||||
<canvas id="memChart"></canvas>
|
||||
<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="memChart">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -118,7 +120,7 @@
|
||||
<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>
|
||||
|
||||
@@ -164,9 +166,15 @@
|
||||
<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-json">{{ stats|safe }}</pre>
|
||||
<pre id="jsonstats" class="language-yaml" style="height:600px;overflow-y:auto;">{{ stats|safe }}</pre>
|
||||
</div>
|
||||
|
||||
<div class="ui text container">
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
input[type=search]::-webkit-search-cancel-button {
|
||||
-webkit-appearance: searchfield-cancel-button;
|
||||
}
|
||||
|
||||
.speech-wrapper {
|
||||
padding-top: 0px;
|
||||
padding: 5px 30px;
|
||||
background-color: #CCCCCC;
|
||||
}
|
||||
|
||||
.bubble-row {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.bubble-row.alt {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.bubble {
|
||||
/*width: 350px; */
|
||||
height: auto;
|
||||
display: block;
|
||||
background: #f5f5f5;
|
||||
border-radius: 4px;
|
||||
box-shadow: 2px 8px 5px #555;
|
||||
position: relative;
|
||||
margin: 0 0 15px;
|
||||
}
|
||||
|
||||
.bubble.alt {
|
||||
margin: 0 0 15px;
|
||||
}
|
||||
|
||||
.bubble-text {
|
||||
padding: 5px 5px 0px 8px;
|
||||
}
|
||||
|
||||
.bubble-name {
|
||||
width: 280px;
|
||||
font-weight: 600;
|
||||
font-size: 12px;
|
||||
margin: 0 0 0px;
|
||||
color: #3498db;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
.material-symbols-rounded {
|
||||
margin-left: auto;
|
||||
font-weight: normal;
|
||||
color: #808080;
|
||||
}
|
||||
}
|
||||
.bubble-name.alt {
|
||||
color: #2ecc71;
|
||||
}
|
||||
|
||||
.bubble-timestamp {
|
||||
margin-right: auto;
|
||||
font-size: 11px;
|
||||
text-transform: uppercase;
|
||||
color: #bbb
|
||||
}
|
||||
|
||||
.bubble-message {
|
||||
font-size: 16px;
|
||||
margin: 0px;
|
||||
padding: 0px 0px 0px 0px;
|
||||
color: #2b2b2b;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.bubble-arrow {
|
||||
position: absolute;
|
||||
width: 0;
|
||||
bottom:30px;
|
||||
left: -16px;
|
||||
height: 0px;
|
||||
}
|
||||
|
||||
.bubble-arrow.alt {
|
||||
right: -2px;
|
||||
bottom: 30px;
|
||||
left: auto;
|
||||
}
|
||||
|
||||
.bubble-arrow:after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
border: 0 solid transparent;
|
||||
border-top: 9px solid #f5f5f5;
|
||||
border-radius: 0 20px 0;
|
||||
width: 15px;
|
||||
height: 30px;
|
||||
transform: rotate(145deg);
|
||||
}
|
||||
.bubble-arrow.alt:after {
|
||||
transform: rotate(45deg) scaleY(-1);
|
||||
}
|
||||
|
||||
.popover {
|
||||
max-width: 400px;
|
||||
}
|
||||
.popover-header {
|
||||
font-size: 8pt;
|
||||
max-width: 400px;
|
||||
padding: 5px;
|
||||
background-color: #ee;
|
||||
}
|
||||
|
||||
.popover-body {
|
||||
white-space: pre-line;
|
||||
max-width: 400px;
|
||||
padding: 5px;
|
||||
}
|
||||
@@ -1,36 +1,9 @@
|
||||
body {
|
||||
background: #eeeeee;
|
||||
margin: 2em;
|
||||
/*margin: 1em;*/
|
||||
text-align: center;
|
||||
font-family: system-ui, sans-serif;
|
||||
}
|
||||
|
||||
footer {
|
||||
padding: 2em;
|
||||
text-align: center;
|
||||
height: 10vh;
|
||||
}
|
||||
|
||||
.ui.segment {
|
||||
background: #eeeeee;
|
||||
}
|
||||
|
||||
ul.list {
|
||||
list-style-type: disc;
|
||||
}
|
||||
ul.list li {
|
||||
list-style-position: outside;
|
||||
}
|
||||
|
||||
#left {
|
||||
margin-right: 2px;
|
||||
height: 300px;
|
||||
}
|
||||
#right {
|
||||
height: 300px;
|
||||
}
|
||||
#center {
|
||||
height: 300px;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
#title {
|
||||
@@ -39,6 +12,7 @@ ul.list li {
|
||||
#version{
|
||||
font-size: .5em;
|
||||
}
|
||||
|
||||
#uptime, #aprsis {
|
||||
font-size: 1em;
|
||||
}
|
||||
@@ -67,28 +41,26 @@ ul.list li {
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
#msgsTabsDiv .ui.tab {
|
||||
margin:0px;
|
||||
padding:0px;
|
||||
display: block;
|
||||
.wc-container {
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
height: 100%;
|
||||
}
|
||||
.wc-container .wc-row {
|
||||
/*border: 1px dotted #0313fc;*/
|
||||
padding: 2px;
|
||||
}
|
||||
.wc-container .wc-row.header {
|
||||
flex: 0 1 auto;
|
||||
}
|
||||
.wc-container .wc-row.content {
|
||||
flex: 1 1 auto;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.wc-container .wc-row.footer {
|
||||
flex: 0 1 0px;
|
||||
}
|
||||
|
||||
#msgsTabsDiv .header, .tiny.text, .content, .break,
|
||||
.thumbs.down.outline.icon,
|
||||
.phone.volume.icon
|
||||
{
|
||||
display: inline-block;
|
||||
float: left;
|
||||
position: relative;
|
||||
}
|
||||
#msgsTabsDiv .tiny.text {
|
||||
width:100px;
|
||||
}
|
||||
#msgsTabsDiv .tiny.header {
|
||||
width:100px;
|
||||
text-align: left;
|
||||
}
|
||||
#msgsTabsDiv .break {
|
||||
margin: 2px;
|
||||
text-align: left;
|
||||
.material-symbols-rounded.md-10 {
|
||||
font-size: 18px !important;
|
||||
}
|
||||
|
||||
@@ -37,5 +37,5 @@
|
||||
border: 1px solid #ccc;
|
||||
height: 450px;
|
||||
overflow-y: scroll;
|
||||
background-color: white;
|
||||
background-color: #CCCCCC;
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
Binary file not shown.
@@ -0,0 +1,23 @@
|
||||
/* 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;
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
/**
|
||||
* jQuery toast plugin created by Kamran Ahmed copyright MIT license 2014
|
||||
*/
|
||||
.jq-toast-wrap { display: block; position: fixed; width: 250px; pointer-events: none !important; margin: 0; padding: 0; letter-spacing: normal; z-index: 9000 !important; }
|
||||
.jq-toast-wrap * { margin: 0; padding: 0; }
|
||||
|
||||
.jq-toast-wrap.bottom-left { bottom: 20px; left: 20px; }
|
||||
.jq-toast-wrap.bottom-right { bottom: 20px; right: 40px; }
|
||||
.jq-toast-wrap.top-left { top: 20px; left: 20px; }
|
||||
.jq-toast-wrap.top-right { top: 20px; right: 40px; }
|
||||
|
||||
.jq-toast-single { display: block; width: 100%; padding: 10px; margin: 0px 0px 5px; border-radius: 4px; font-size: 12px; font-family: arial, sans-serif; line-height: 17px; position: relative; pointer-events: all !important; background-color: #444444; color: white; }
|
||||
|
||||
.jq-toast-single h2 { font-family: arial, sans-serif; font-size: 14px; margin: 0px 0px 7px; background: none; color: inherit; line-height: inherit; letter-spacing: normal; }
|
||||
.jq-toast-single a { color: #eee; text-decoration: none; font-weight: bold; border-bottom: 1px solid white; padding-bottom: 3px; font-size: 12px; }
|
||||
|
||||
.jq-toast-single ul { margin: 0px 0px 0px 15px; background: none; padding:0px; }
|
||||
.jq-toast-single ul li { list-style-type: disc !important; line-height: 17px; background: none; margin: 0; padding: 0; letter-spacing: normal; }
|
||||
|
||||
.close-jq-toast-single { position: absolute; top: 3px; right: 7px; font-size: 14px; cursor: pointer; }
|
||||
|
||||
.jq-toast-loader { display: block; position: absolute; top: -2px; height: 5px; width: 0%; left: 0; border-radius: 5px; background: red; }
|
||||
.jq-toast-loaded { width: 100%; }
|
||||
.jq-has-icon { padding: 10px 10px 10px 50px; background-repeat: no-repeat; background-position: 10px; }
|
||||
.jq-icon-info { background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAGwSURBVEhLtZa9SgNBEMc9sUxxRcoUKSzSWIhXpFMhhYWFhaBg4yPYiWCXZxBLERsLRS3EQkEfwCKdjWJAwSKCgoKCcudv4O5YLrt7EzgXhiU3/4+b2ckmwVjJSpKkQ6wAi4gwhT+z3wRBcEz0yjSseUTrcRyfsHsXmD0AmbHOC9Ii8VImnuXBPglHpQ5wwSVM7sNnTG7Za4JwDdCjxyAiH3nyA2mtaTJufiDZ5dCaqlItILh1NHatfN5skvjx9Z38m69CgzuXmZgVrPIGE763Jx9qKsRozWYw6xOHdER+nn2KkO+Bb+UV5CBN6WC6QtBgbRVozrahAbmm6HtUsgtPC19tFdxXZYBOfkbmFJ1VaHA1VAHjd0pp70oTZzvR+EVrx2Ygfdsq6eu55BHYR8hlcki+n+kERUFG8BrA0BwjeAv2M8WLQBtcy+SD6fNsmnB3AlBLrgTtVW1c2QN4bVWLATaIS60J2Du5y1TiJgjSBvFVZgTmwCU+dAZFoPxGEEs8nyHC9Bwe2GvEJv2WXZb0vjdyFT4Cxk3e/kIqlOGoVLwwPevpYHT+00T+hWwXDf4AJAOUqWcDhbwAAAAASUVORK5CYII='); background-color: #31708f; color: #d9edf7; border-color: #bce8f1; }
|
||||
.jq-icon-warning { background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAGYSURBVEhL5ZSvTsNQFMbXZGICMYGYmJhAQIJAICYQPAACiSDB8AiICQQJT4CqQEwgJvYASAQCiZiYmJhAIBATCARJy+9rTsldd8sKu1M0+dLb057v6/lbq/2rK0mS/TRNj9cWNAKPYIJII7gIxCcQ51cvqID+GIEX8ASG4B1bK5gIZFeQfoJdEXOfgX4QAQg7kH2A65yQ87lyxb27sggkAzAuFhbbg1K2kgCkB1bVwyIR9m2L7PRPIhDUIXgGtyKw575yz3lTNs6X4JXnjV+LKM/m3MydnTbtOKIjtz6VhCBq4vSm3ncdrD2lk0VgUXSVKjVDJXJzijW1RQdsU7F77He8u68koNZTz8Oz5yGa6J3H3lZ0xYgXBK2QymlWWA+RWnYhskLBv2vmE+hBMCtbA7KX5drWyRT/2JsqZ2IvfB9Y4bWDNMFbJRFmC9E74SoS0CqulwjkC0+5bpcV1CZ8NMej4pjy0U+doDQsGyo1hzVJttIjhQ7GnBtRFN1UarUlH8F3xict+HY07rEzoUGPlWcjRFRr4/gChZgc3ZL2d8oAAAAASUVORK5CYII='); background-color: #8a6d3b; color: #fcf8e3; border-color: #faebcc; }
|
||||
.jq-icon-error { background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAHOSURBVEhLrZa/SgNBEMZzh0WKCClSCKaIYOED+AAKeQQLG8HWztLCImBrYadgIdY+gIKNYkBFSwu7CAoqCgkkoGBI/E28PdbLZmeDLgzZzcx83/zZ2SSXC1j9fr+I1Hq93g2yxH4iwM1vkoBWAdxCmpzTxfkN2RcyZNaHFIkSo10+8kgxkXIURV5HGxTmFuc75B2RfQkpxHG8aAgaAFa0tAHqYFfQ7Iwe2yhODk8+J4C7yAoRTWI3w/4klGRgR4lO7Rpn9+gvMyWp+uxFh8+H+ARlgN1nJuJuQAYvNkEnwGFck18Er4q3egEc/oO+mhLdKgRyhdNFiacC0rlOCbhNVz4H9FnAYgDBvU3QIioZlJFLJtsoHYRDfiZoUyIxqCtRpVlANq0EU4dApjrtgezPFad5S19Wgjkc0hNVnuF4HjVA6C7QrSIbylB+oZe3aHgBsqlNqKYH48jXyJKMuAbiyVJ8KzaB3eRc0pg9VwQ4niFryI68qiOi3AbjwdsfnAtk0bCjTLJKr6mrD9g8iq/S/B81hguOMlQTnVyG40wAcjnmgsCNESDrjme7wfftP4P7SP4N3CJZdvzoNyGq2c/HWOXJGsvVg+RA/k2MC/wN6I2YA2Pt8GkAAAAASUVORK5CYII='); background-color: #a94442; color: #f2dede; border-color: #ebccd1; }
|
||||
.jq-icon-success { background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAADsSURBVEhLY2AYBfQMgf///3P8+/evAIgvA/FsIF+BavYDDWMBGroaSMMBiE8VC7AZDrIFaMFnii3AZTjUgsUUWUDA8OdAH6iQbQEhw4HyGsPEcKBXBIC4ARhex4G4BsjmweU1soIFaGg/WtoFZRIZdEvIMhxkCCjXIVsATV6gFGACs4Rsw0EGgIIH3QJYJgHSARQZDrWAB+jawzgs+Q2UO49D7jnRSRGoEFRILcdmEMWGI0cm0JJ2QpYA1RDvcmzJEWhABhD/pqrL0S0CWuABKgnRki9lLseS7g2AlqwHWQSKH4oKLrILpRGhEQCw2LiRUIa4lwAAAABJRU5ErkJggg=='); color: #dff0d8; background-color: #3c763d; border-color: #d6e9c6; }
|
||||
File diff suppressed because one or more lines are too long
@@ -70,10 +70,11 @@ function showPosition(position) {
|
||||
}
|
||||
console.log(msg);
|
||||
$.toast({
|
||||
title: 'Sending GPS Beacon',
|
||||
message: "Latitude: "+position.coords.latitude+"<br>Longitude: "+position.coords.longitude,
|
||||
showProgress: 'bottom',
|
||||
classProgress: 'red'
|
||||
heading: 'Sending GPS Beacon',
|
||||
text: "Latitude: "+position.coords.latitude+"<br>Longitude: "+position.coords.longitude,
|
||||
loader: true,
|
||||
loaderBg: '#9EC600',
|
||||
position: 'top-center',
|
||||
});
|
||||
|
||||
console.log("Sending GPS msg")
|
||||
|
||||
@@ -1,246 +0,0 @@
|
||||
var cleared = false;
|
||||
var callsign_list = {};
|
||||
var message_list = {};
|
||||
var from_msg_list = {};
|
||||
const socket = io("/sendmsg");
|
||||
|
||||
function size_dict(d){c=0; for (i in d) ++c; return c}
|
||||
|
||||
function init_chat() {
|
||||
socket.on('connect', function () {
|
||||
console.log("Connected to socketio");
|
||||
});
|
||||
socket.on('connected', function(msg) {
|
||||
console.log("Connected!");
|
||||
console.log(msg);
|
||||
});
|
||||
|
||||
socket.on("sent", function(msg) {
|
||||
if (cleared == false) {
|
||||
var msgsdiv = $("#msgsTabsDiv");
|
||||
msgsdiv.html('')
|
||||
cleared = true
|
||||
}
|
||||
sent_msg(msg);
|
||||
});
|
||||
|
||||
socket.on("ack", function(msg) {
|
||||
update_msg(msg);
|
||||
});
|
||||
|
||||
socket.on("new", function(msg) {
|
||||
if (cleared == false) {
|
||||
var msgsdiv = $("#msgsTabsDiv");
|
||||
msgsdiv.html('')
|
||||
cleared = true
|
||||
}
|
||||
from_msg(msg);
|
||||
});
|
||||
|
||||
$("#sendform").submit(function(event) {
|
||||
event.preventDefault();
|
||||
msg = {'to': $('#to_call').val().toUpperCase(),
|
||||
'message': $('#message').val(),
|
||||
}
|
||||
socket.emit("send", msg);
|
||||
$('#message').val('');
|
||||
});
|
||||
|
||||
init_gps();
|
||||
}
|
||||
|
||||
|
||||
function add_callsign(callsign) {
|
||||
/* Ensure a callsign exists in the left hand nav */
|
||||
dropdown = $('#callsign_dropdown')
|
||||
|
||||
if (callsign in callsign_list) {
|
||||
console.log(callsign+' already in list.')
|
||||
return false
|
||||
}
|
||||
|
||||
var callsignTabs = $("#callsignTabs");
|
||||
tab_name = tab_string(callsign);
|
||||
tab_content = tab_content_name(callsign);
|
||||
divname = content_divname(callsign);
|
||||
|
||||
item_html = '<div class="active item" id="'+tab_name+'" onclick="openCallsign(event, \''+callsign+'\');">'+callsign+'</div>';
|
||||
callsignTabs.append(item_html);
|
||||
|
||||
callsign_list[callsign] = {'name': callsign, 'value': callsign, 'text': callsign}
|
||||
return true
|
||||
}
|
||||
|
||||
function append_message(callsign, msg, msg_html) {
|
||||
console.log('append_message');
|
||||
new_callsign = false
|
||||
if (!message_list.hasOwnProperty(callsign)) {
|
||||
message_list[callsign] = new Array();
|
||||
}
|
||||
message_list[callsign].push(msg);
|
||||
|
||||
// Find the right div to place the html
|
||||
new_callsign = add_callsign(callsign);
|
||||
append_message_html(callsign, msg_html, new_callsign);
|
||||
if (new_callsign) {
|
||||
//click on the new tab
|
||||
click_div = '#'+tab_string(callsign);
|
||||
console.log("Click on "+click_div);
|
||||
$(click_div).click();
|
||||
}
|
||||
}
|
||||
|
||||
function tab_string(callsign) {
|
||||
return "msgs"+callsign;
|
||||
}
|
||||
|
||||
function tab_content_name(callsign) {
|
||||
return tab_string(callsign)+"Content";
|
||||
}
|
||||
|
||||
function content_divname(callsign) {
|
||||
return "#"+tab_content_name(callsign);
|
||||
}
|
||||
|
||||
function append_message_html(callsign, msg_html, new_callsign) {
|
||||
var msgsTabs = $('#msgsTabsDiv');
|
||||
divname_str = tab_content_name(callsign);
|
||||
divname = content_divname(callsign);
|
||||
if (new_callsign) {
|
||||
// we have to add a new DIV
|
||||
msg_div_html = '<div class="tabcontent" id="'+divname_str+'" style="height:450px;">'+msg_html+'</div>';
|
||||
msgsTabs.append(msg_div_html);
|
||||
} else {
|
||||
var msgDiv = $(divname);
|
||||
msgDiv.append(msg_html);
|
||||
}
|
||||
|
||||
$(divname).animate({scrollTop: $(divname)[0].scrollHeight}, "slow");
|
||||
}
|
||||
|
||||
function create_message_html(time, from, to, message, ack, msg) {
|
||||
div_id = from + "_" + msg.id;
|
||||
msg_html = '<div class="item" id="'+div_id+'">';
|
||||
msg_html += '<div class="tiny text">'+time+'</div>';
|
||||
msg_html += '<div class="middle aligned content">';
|
||||
msg_html += '<div class="tiny red header">'+from+'</div>';
|
||||
if (ack) {
|
||||
msg_html += '<i class="thumbs down outline icon" id="' + ack_id + '" data-content="Waiting for ACK"></i>';
|
||||
} else {
|
||||
msg_html += '<i class="phone volume icon" data-content="Recieved Message"></i>';
|
||||
}
|
||||
msg_html += '<div class="middle aligned content">> </div>';
|
||||
msg_html += '</div>';
|
||||
msg_html += '<div class="middle aligned content">'+message+'</div>';
|
||||
msg_html += '</div><br>';
|
||||
|
||||
return msg_html
|
||||
}
|
||||
|
||||
function flash_message(msg) {
|
||||
// Callback function to bring a hidden box back
|
||||
id = msg.from + "_" + msg.id;
|
||||
var msgid = $('#'+id);
|
||||
msgid.effect("pulsate", { times:3 }, 2000);
|
||||
}
|
||||
|
||||
function sent_msg(msg) {
|
||||
var msgsdiv = $("#sendMsgsDiv");
|
||||
|
||||
ts_str = msg["ts"].toString();
|
||||
ts = ts_str.split(".")[0]*1000;
|
||||
id = ts_str.split('.')[0]
|
||||
ack_id = "ack_" + id
|
||||
|
||||
var d = new Date(ts).toLocaleDateString("en-US")
|
||||
var t = new Date(ts).toLocaleTimeString("en-US")
|
||||
|
||||
msg_html = create_message_html(t, msg['from'], msg['to'], msg['message'], ack_id, msg);
|
||||
append_message(msg['to'], msg, msg_html);
|
||||
}
|
||||
|
||||
function from_msg(msg) {
|
||||
var msgsdiv = $("#sendMsgsDiv");
|
||||
console.log(msg);
|
||||
if (!from_msg_list.hasOwnProperty(msg.from)) {
|
||||
from_msg_list[msg.from] = new Array();
|
||||
}
|
||||
|
||||
if (msg.id in from_msg_list[msg.from]) {
|
||||
// We already have this message
|
||||
console.log("We already have this message " + msg);
|
||||
// Do some flashy thing?
|
||||
flash_message(msg);
|
||||
return false
|
||||
} else {
|
||||
console.log("Adding message " + msg.id + " to " + msg.from);
|
||||
from_msg_list[msg.from][msg.id] = msg
|
||||
}
|
||||
|
||||
// We have an existing entry
|
||||
ts_str = msg["ts"].toString();
|
||||
ts = ts_str.split(".")[0]*1000;
|
||||
id = ts_str.split('.')[0]
|
||||
ack_id = "ack_" + id
|
||||
|
||||
var d = new Date(ts).toLocaleDateString("en-US")
|
||||
var t = new Date(ts).toLocaleTimeString("en-US")
|
||||
|
||||
from = msg['from']
|
||||
msg_html = create_message_html(t, from, false, msg['message'], false, msg);
|
||||
append_message(from, msg, msg_html);
|
||||
}
|
||||
|
||||
function update_msg(msg) {
|
||||
var msgsdiv = $("#sendMsgsDiv");
|
||||
// We have an existing entry
|
||||
ts_str = msg["ts"].toString();
|
||||
id = ts_str.split('.')[0]
|
||||
pretty_id = "pretty_" + id
|
||||
loader_id = "loader_" + id
|
||||
ack_id = "ack_" + id
|
||||
span_id = "span_" + id
|
||||
|
||||
|
||||
|
||||
if (msg['ack'] == true) {
|
||||
var loader_div = $('#' + loader_id);
|
||||
var ack_div = $('#' + ack_id);
|
||||
loader_div.removeClass('ui active inline loader');
|
||||
loader_div.addClass('ui disabled loader');
|
||||
ack_div.removeClass('thumbs up outline icon');
|
||||
ack_div.addClass('thumbs up outline icon');
|
||||
}
|
||||
|
||||
$('.ui.accordion').accordion('refresh');
|
||||
}
|
||||
|
||||
function callsign_select(callsign) {
|
||||
var tocall = $("#to_call");
|
||||
tocall.val(callsign);
|
||||
}
|
||||
|
||||
function reset_Tabs() {
|
||||
tabcontent = document.getElementsByClassName("tabcontent");
|
||||
for (i = 0; i < tabcontent.length; i++) {
|
||||
tabcontent[i].style.display = "none";
|
||||
}
|
||||
}
|
||||
|
||||
function openCallsign(evt, callsign) {
|
||||
var i, tabcontent, tablinks;
|
||||
|
||||
tab_content = tab_content_name(callsign);
|
||||
|
||||
tabcontent = document.getElementsByClassName("tabcontent");
|
||||
for (i = 0; i < tabcontent.length; i++) {
|
||||
tabcontent[i].style.display = "none";
|
||||
}
|
||||
tablinks = document.getElementsByClassName("tablinks");
|
||||
for (i = 0; i < tablinks.length; i++) {
|
||||
tablinks[i].className = tablinks[i].className.replace(" active", "");
|
||||
}
|
||||
document.getElementById(tab_content).style.display = "block";
|
||||
evt.target.className += " active";
|
||||
callsign_select(callsign);
|
||||
}
|
||||
@@ -2,6 +2,7 @@ var cleared = false;
|
||||
var callsign_list = {};
|
||||
var message_list = {};
|
||||
var from_msg_list = {};
|
||||
var selected_tab_callsign = null;
|
||||
const socket = io("/sendmsg");
|
||||
|
||||
MSG_TYPE_TX = "tx";
|
||||
@@ -10,6 +11,16 @@ MSG_TYPE_ACK = "ack";
|
||||
|
||||
function size_dict(d){c=0; for (i in d) ++c; return c}
|
||||
|
||||
function raise_error(msg) {
|
||||
$.toast({
|
||||
heading: 'Error',
|
||||
text: msg,
|
||||
loader: true,
|
||||
loaderBg: '#9EC600',
|
||||
position: 'top-center',
|
||||
});
|
||||
}
|
||||
|
||||
function init_chat() {
|
||||
socket.on('connect', function () {
|
||||
console.log("Connected to socketio");
|
||||
@@ -20,8 +31,9 @@ function init_chat() {
|
||||
});
|
||||
|
||||
socket.on("sent", function(msg) {
|
||||
console.log("SENT: ");
|
||||
console.log(msg);
|
||||
if (cleared === false) {
|
||||
console.log("CLEARING #msgsTabsDiv");
|
||||
var msgsdiv = $("#msgsTabsDiv");
|
||||
msgsdiv.html('');
|
||||
cleared = true;
|
||||
@@ -47,11 +59,22 @@ function init_chat() {
|
||||
|
||||
$("#sendform").submit(function(event) {
|
||||
event.preventDefault();
|
||||
msg = {'to': $('#to_call').val(),
|
||||
'message': $('#message').val(),
|
||||
}
|
||||
socket.emit("send", msg);
|
||||
$('#message').val('');
|
||||
to_call = $('#to_call').val();
|
||||
message = $('#message').val();
|
||||
path = $('#pkt_path option:selected').val();
|
||||
if (to_call == "") {
|
||||
raise_error("You must enter a callsign to send a message")
|
||||
return false;
|
||||
} else {
|
||||
if (message == "") {
|
||||
raise_error("You must enter a message to send")
|
||||
return false;
|
||||
}
|
||||
msg = {'to': to_call, 'message': message, 'path': path};
|
||||
console.log(msg);
|
||||
socket.emit("send", msg);
|
||||
$('#message').val('');
|
||||
}
|
||||
});
|
||||
|
||||
init_gps();
|
||||
@@ -59,9 +82,58 @@ function init_chat() {
|
||||
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 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["ts"].toString();
|
||||
ts_str = msg["timestamp"].toString();
|
||||
ts = ts_str.split(".")[0]*1000;
|
||||
id = ts_str.split('.')[0];
|
||||
return {'timestamp': ts, 'id': id};
|
||||
@@ -89,25 +161,25 @@ function init_messages() {
|
||||
// This tries to load any previous conversations from local storage
|
||||
callsign_list = JSON.parse(localStorage.getItem('callsign_list'));
|
||||
message_list = JSON.parse(localStorage.getItem('message_list'));
|
||||
console.log("init_messages");
|
||||
if (callsign_list == null) {
|
||||
callsign_list = {};
|
||||
}
|
||||
if (message_list == null) {
|
||||
message_list = {};
|
||||
}
|
||||
console.log(callsign_list);
|
||||
console.log(message_list);
|
||||
//console.log(callsign_list);
|
||||
//console.log(message_list);
|
||||
|
||||
// Now loop through each callsign and add the tabs
|
||||
first_callsign = null;
|
||||
for (callsign in callsign_list) {
|
||||
console.log("Adding callsign " + callsign);
|
||||
if (first_callsign === null) {
|
||||
first_callsign = callsign;
|
||||
console.log("first_callsign " + first_callsign)
|
||||
active = true;
|
||||
} else {
|
||||
active = false;
|
||||
}
|
||||
create_callsign_tab(callsign);
|
||||
create_callsign_tab(callsign, active);
|
||||
}
|
||||
// and then populate the messages in order
|
||||
for (callsign in message_list) {
|
||||
@@ -117,54 +189,139 @@ function init_messages() {
|
||||
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(t, msg['from'], msg['to'], msg['message'], ack_id, msg, acked);
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
//Click on the very first tab
|
||||
if (first_callsign !== null) {
|
||||
click_div = '#'+tab_string(first_callsign);
|
||||
var click_timer = setTimeout(function() {
|
||||
console.log("Click on first tab " + click_div);
|
||||
$(click_div).click();
|
||||
clearTimeout(click_timer);
|
||||
}, 500);
|
||||
callsign_select(first_callsign);
|
||||
}
|
||||
}
|
||||
|
||||
function create_callsign_tab(callsign) {
|
||||
//Create the html for the callsign tab and insert it into the DOM
|
||||
console.log("create_callsign_tab " + callsign)
|
||||
var callsignTabs = $("#callsignTabs");
|
||||
tab_name = tab_string(callsign);
|
||||
tab_content = tab_content_name(callsign);
|
||||
divname = content_divname(callsign);
|
||||
function scroll_main_content(callsign=false) {
|
||||
var wc = $('#wc-content');
|
||||
var d = $('#msgsTabContent');
|
||||
var scrollHeight = wc.prop('scrollHeight');
|
||||
var clientHeight = wc.prop('clientHeight');
|
||||
|
||||
item_html = '<div class="tablinks" id="'+tab_name+'" onclick="openCallsign(event, \''+callsign+'\');">'+callsign+'</div>';
|
||||
console.log(item_html);
|
||||
callsignTabs.append(item_html);
|
||||
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 add_callsign(callsign) {
|
||||
function create_callsign_tab(callsign, active=false) {
|
||||
//Create the html for the callsign tab and insert it into the DOM
|
||||
var callsignTabs = $("#msgsTabList");
|
||||
tab_id = tab_string(callsign);
|
||||
tab_id_li = tab_li_string(callsign);
|
||||
tab_notify_id = tab_notification_id(callsign);
|
||||
tab_content = tab_content_name(callsign);
|
||||
if (active) {
|
||||
active_str = "active";
|
||||
} else {
|
||||
active_str = "";
|
||||
}
|
||||
|
||||
item_html = '<li class="nav-item" role="presentation" callsign="'+callsign+'" id="'+tab_id_li+'">';
|
||||
//item_html += '<button onClick="callsign_select(\''+callsign+'\');" callsign="'+callsign+'" class="nav-link '+active_str+'" id="'+tab_id+'" data-bs-toggle="tab" data-bs-target="#'+tab_content+'" type="button" role="tab" aria-controls="'+callsign+'" aria-selected="true">';
|
||||
item_html += '<button onClick="callsign_select(\''+callsign+'\');" callsign="'+callsign+'" class="nav-link position-relative '+active_str+'" id="'+tab_id+'" data-bs-toggle="tab" data-bs-target="#'+tab_content+'" type="button" role="tab" aria-controls="'+callsign+'" aria-selected="true">';
|
||||
item_html += callsign+' ';
|
||||
item_html += '<span id="'+tab_notify_id+'" class="position-absolute top-0 start-80 translate-middle badge bg-danger border border-light rounded-pill visually-hidden">0</span>';
|
||||
item_html += '<span onclick="delete_tab(\''+callsign+'\');">×</span>';
|
||||
item_html += '</button></li>'
|
||||
callsignTabs.append(item_html);
|
||||
create_callsign_tab_content(callsign, active);
|
||||
}
|
||||
|
||||
function create_callsign_tab_content(callsign, active=false) {
|
||||
var callsignTabsContent = $("#msgsTabContent");
|
||||
tab_id = tab_string(callsign);
|
||||
tab_content = tab_content_name(callsign);
|
||||
wrapper_id = tab_content_speech_wrapper(callsign);
|
||||
if (active) {
|
||||
active_str = "show active";
|
||||
} else {
|
||||
active_str = '';
|
||||
}
|
||||
|
||||
item_html = '<div class="tab-pane fade '+active_str+'" id="'+tab_content+'" role="tabpanel" aria-labelledby="'+tab_id+'">';
|
||||
item_html += '<div class="speech-wrapper" id="'+wrapper_id+'"></div>';
|
||||
item_html += '</div>';
|
||||
callsignTabsContent.append(item_html);
|
||||
}
|
||||
|
||||
function delete_tab(callsign) {
|
||||
// User asked to delete the tab and the conversation
|
||||
tab_id = tab_string(callsign, true);
|
||||
tab_id_li = tab_li_string(callsign, true);
|
||||
tab_content = tab_content_name(callsign, true);
|
||||
$(tab_id_li).remove();
|
||||
$(tab_content).remove();
|
||||
delete callsign_list[callsign];
|
||||
delete message_list[callsign];
|
||||
|
||||
// Now select the first tab
|
||||
first_tab = $("#msgsTabList").children().first().children().first();
|
||||
console.log(first_tab);
|
||||
$(first_tab).click();
|
||||
save_data();
|
||||
}
|
||||
|
||||
function add_callsign(callsign, msg) {
|
||||
/* Ensure a callsign exists in the left hand nav */
|
||||
if (callsign in callsign_list) {
|
||||
return false
|
||||
}
|
||||
create_callsign_tab(callsign);
|
||||
callsign_list[callsign] = true;
|
||||
return true
|
||||
len = Object.keys(callsign_list).length;
|
||||
if (len == 0) {
|
||||
active = true;
|
||||
} else {
|
||||
active = false;
|
||||
}
|
||||
create_callsign_tab(callsign, active);
|
||||
callsign_list[callsign] = '';
|
||||
return true;
|
||||
}
|
||||
|
||||
function update_callsign_path(callsign, path) {
|
||||
//Get the selected path to save for this callsign
|
||||
path = msg['path']
|
||||
console.log("Path is " + path);
|
||||
$('#pkt_path').val(path);
|
||||
callsign_list[callsign] = path;
|
||||
|
||||
}
|
||||
|
||||
function append_message(callsign, msg, msg_html) {
|
||||
console.log("append_message " + callsign + " " + msg + " " + msg_html);
|
||||
new_callsign = false
|
||||
if (!message_list.hasOwnProperty(callsign)) {
|
||||
//message_list[callsign] = new Array();
|
||||
@@ -173,149 +330,152 @@ function append_message(callsign, msg, msg_html) {
|
||||
ts_id = message_ts_id(msg);
|
||||
id = ts_id['id']
|
||||
message_list[callsign][id] = msg;
|
||||
if (selected_tab_callsign != callsign) {
|
||||
// We need to update the notification for the tab
|
||||
tab_notify_id = tab_notification_id(callsign, true);
|
||||
// get the current count of notifications
|
||||
count = parseInt($(tab_notify_id).text());
|
||||
count += 1;
|
||||
$(tab_notify_id).text(count);
|
||||
$(tab_notify_id).removeClass('visually-hidden');
|
||||
}
|
||||
|
||||
// Find the right div to place the html
|
||||
new_callsign = add_callsign(callsign);
|
||||
|
||||
new_callsign = add_callsign(callsign, msg);
|
||||
update_callsign_path(callsign, msg['path']);
|
||||
append_message_html(callsign, msg_html, new_callsign);
|
||||
if (new_callsign) {
|
||||
//click on the new tab
|
||||
click_div = '#'+tab_string(callsign);
|
||||
$(click_div).click();
|
||||
//Now click the tab
|
||||
callsign_tab_id = callsign_tab(callsign);
|
||||
$(callsign_tab_id).click();
|
||||
callsign_select(callsign);
|
||||
}
|
||||
}
|
||||
|
||||
function tab_string(callsign) {
|
||||
return "msgs"+callsign;
|
||||
}
|
||||
|
||||
function tab_content_name(callsign) {
|
||||
return tab_string(callsign)+"Content";
|
||||
}
|
||||
|
||||
function content_divname(callsign) {
|
||||
return "#"+tab_content_name(callsign);
|
||||
}
|
||||
|
||||
function append_message_html(callsign, msg_html, new_callsign) {
|
||||
var msgsTabs = $('#msgsTabsDiv');
|
||||
console.log("append_message_html " + callsign + " " + msg_html + " " + new_callsign);
|
||||
divname_str = tab_content_name(callsign);
|
||||
divname = content_divname(callsign);
|
||||
if (new_callsign) {
|
||||
// we have to add a new DIV
|
||||
console.log("new callsign, create msg_div_html ");
|
||||
msg_div_html = '<div class="tabcontent" id="'+divname_str+'" style="height:450px;">'+msg_html+'</div>';
|
||||
console.log(msg_div_html);
|
||||
msgsTabs.append(msg_div_html);
|
||||
} else {
|
||||
var msgDiv = $(divname);
|
||||
console.log("Appending ("+ msg_html + ") to " + divname);
|
||||
console.log(msgDiv);
|
||||
msgDiv.append(msg_html);
|
||||
console.log(msgDiv);
|
||||
}
|
||||
console.log("divname " + divname);
|
||||
console.log($(divname).length);
|
||||
tab_content = tab_content_name(callsign);
|
||||
wrapper_id = tab_content_speech_wrapper_id(callsign);
|
||||
|
||||
if ($(divname).length > 0) {
|
||||
$(divname).animate({scrollTop: $(divname)[0].scrollHeight}, "slow");
|
||||
$(wrapper_id).append(msg_html);
|
||||
|
||||
if ($(wrapper_id).children().length > 0) {
|
||||
$(wrapper_id).animate({scrollTop: $(wrapper_id)[0].scrollHeight}, "fast");
|
||||
}
|
||||
$(divname).trigger('click');
|
||||
}
|
||||
|
||||
function create_message_html(time, from, to, message, ack_id, msg, acked=false) {
|
||||
div_id = from + "_" + msg.id;
|
||||
msg_html = '<div class="item" id="'+div_id+'">';
|
||||
msg_html += '<div class="tiny text">'+time+'</div>';
|
||||
msg_html += '<div class="middle aligned content">';
|
||||
msg_html += '<div class="tiny red header">'+from+'</div>';
|
||||
function create_message_html(date, time, from, to, message, ack_id, msg, acked=false) {
|
||||
div_id = from + "_" + msg.msgNo;
|
||||
if (ack_id) {
|
||||
alt = " alt"
|
||||
} else {
|
||||
alt = ""
|
||||
}
|
||||
|
||||
bubble_class = "bubble" + alt + " text-nowrap"
|
||||
bubble_name_class = "bubble-name" + alt
|
||||
bubble_msgid = bubble_msg_id(msg);
|
||||
date_str = date + " " + time;
|
||||
sane_date_str = date_str.replace(/ /g,"").replaceAll("/","").replaceAll(":","");
|
||||
|
||||
msg_html = '<div class="bubble-row'+alt+'">';
|
||||
msg_html += '<div id="'+bubble_msgid+'" class="'+ bubble_class + '" data-bs-toggle="popover" data-bs-content="'+msg['raw']+'">';
|
||||
msg_html += '<div class="bubble-text">';
|
||||
msg_html += '<p class="'+ bubble_name_class +'">'+from+' ';
|
||||
msg_html += '<span class="bubble-timestamp">'+date_str+'</span>';
|
||||
if (ack_id) {
|
||||
if (acked) {
|
||||
msg_html += '<div class="middle aligned content"><i class="thumbs up outline icon" id="' + ack_id + '" data-content="Message ACKed"></i></div>';
|
||||
msg_html += '<span class="material-symbols-rounded md-10" id="' + ack_id + '">thumb_up</span>';
|
||||
} else {
|
||||
msg_html += '<div class="middle aligned content"><i class="thumbs down outline icon" id="' + ack_id + '" data-content="Waiting for ACK"></i></div>';
|
||||
msg_html += '<span class="material-symbols-rounded md-10" id="' + ack_id + '">thumb_down</span>';
|
||||
}
|
||||
} else {
|
||||
msg_html += '<i class="phone volume icon" data-content="Recieved Message"></i>';
|
||||
}
|
||||
msg_html += '<div class="middle aligned content">> </div>';
|
||||
msg_html += '</div>';
|
||||
msg_html += '<div class="middle aligned content">'+message+'</div>';
|
||||
msg_html += '</div><br>';
|
||||
msg_html += "</p>";
|
||||
bubble_msg_class = "bubble-message"
|
||||
if (ack_id) {
|
||||
bubble_arrow_class = "bubble-arrow alt"
|
||||
popover_placement = "left"
|
||||
} else {
|
||||
bubble_arrow_class = "bubble-arrow"
|
||||
popover_placement = "right"
|
||||
}
|
||||
|
||||
return msg_html
|
||||
msg_html += '<p class="' +bubble_msg_class+ '">'+message+'</p>';
|
||||
msg_html += '<div class="'+ bubble_arrow_class + '"></div>';
|
||||
msg_html += "</div></div></div>";
|
||||
|
||||
popover_html = '\n<script>$(function () {$(\'[data-bs-toggle="popover"]\').popover('
|
||||
popover_html += '{title: "APRS Raw Packet", html: false, trigger: \'hover\', placement: \''+popover_placement+'\'});})';
|
||||
popover_html += '</script>'
|
||||
|
||||
return msg_html+popover_html
|
||||
}
|
||||
|
||||
function flash_message(msg) {
|
||||
// Callback function to bring a hidden box back
|
||||
id = msg.from + "_" + msg.id;
|
||||
var msgid = $('#'+id);
|
||||
msgid.effect("pulsate", { times:3 }, 2000);
|
||||
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(t, msg['from'], msg['to'], msg['message'], ack_id, msg, false);
|
||||
append_message(msg['to'], msg, msg_html);
|
||||
msg_html = create_message_html(d, t, msg['from_call'], msg['to_call'], msg['message_text'], ack_id, msg, false);
|
||||
append_message(msg['to_call'], msg, msg_html);
|
||||
save_data();
|
||||
scroll_main_content(msg['from_call']);
|
||||
}
|
||||
|
||||
function from_msg(msg) {
|
||||
console.log(msg);
|
||||
if (!from_msg_list.hasOwnProperty(msg.from)) {
|
||||
from_msg_list[msg.from] = new Array();
|
||||
if (!from_msg_list.hasOwnProperty(msg["from_call"])) {
|
||||
from_msg_list[msg["from_call"]] = new Array();
|
||||
}
|
||||
|
||||
if (msg.id in from_msg_list[msg.from]) {
|
||||
if (msg["msgNo"] in from_msg_list[msg["from_call"]]) {
|
||||
// We already have this message
|
||||
console.log("We already have this message " + msg);
|
||||
console.log("We already have this message msgNo=" + msg["msgNo"]);
|
||||
// Do some flashy thing?
|
||||
flash_message(msg);
|
||||
return false
|
||||
} else {
|
||||
console.log("Adding message " + msg.id + " to " + msg.from);
|
||||
from_msg_list[msg.from][msg.id] = msg
|
||||
console.log("Adding message " + msg["msgNo"] + " to " + msg["from_call"]);
|
||||
from_msg_list[msg["from_call"]][msg["msgNo"]] = msg
|
||||
}
|
||||
|
||||
info = time_ack_from_msg(msg);
|
||||
t = info['time'];
|
||||
d = info['date'];
|
||||
ack_id = info['ack_id'];
|
||||
|
||||
from = msg['from']
|
||||
msg_html = create_message_html(t, from, false, msg['message'], false, msg, false);
|
||||
from = msg['from_call']
|
||||
msg_html = create_message_html(d, t, from, false, msg['message_text'], false, msg, false);
|
||||
append_message(from, msg, msg_html);
|
||||
save_data();
|
||||
scroll_main_content(from);
|
||||
}
|
||||
|
||||
function ack_msg(msg) {
|
||||
// Acknowledge a message
|
||||
console.log("ack_msg ");
|
||||
console.log(msg);
|
||||
console.log(message_list);
|
||||
|
||||
// We have an existing entry
|
||||
ts_id = message_ts_id(msg);
|
||||
console.log(ts_id)
|
||||
id = ts_id['id'];
|
||||
//Mark the message as acked
|
||||
callsign = msg['to'];
|
||||
callsign = msg['to_call'];
|
||||
// Ensure the message_list has this callsign
|
||||
if (!message_list.hasOwnProperty(callsign)) {
|
||||
console.log("No message_list for " + callsign);
|
||||
return false
|
||||
}
|
||||
// Ensure the message_list has this id
|
||||
if (!message_list[callsign].hasOwnProperty(id)) {
|
||||
console.log("No message_list for " + callsign + " " + id);
|
||||
return false
|
||||
}
|
||||
console.log("Marking message as acked " + callsign + " " + id)
|
||||
if (message_list[callsign][id]['ack'] == true) {
|
||||
console.log("Message already acked");
|
||||
return false;
|
||||
}
|
||||
message_list[callsign][id]['ack'] = true;
|
||||
@@ -323,46 +483,22 @@ function ack_msg(msg) {
|
||||
|
||||
if (msg['ack'] == true) {
|
||||
var ack_div = $('#' + ack_id);
|
||||
//ack_div.removeClass('thumbs up outline icon');
|
||||
ack_div.removeClass('thumbs down outline icon');
|
||||
ack_div.addClass('thumbs up outline icon');
|
||||
ack_div.html('thumb_up');
|
||||
}
|
||||
|
||||
$('.ui.accordion').accordion('refresh');
|
||||
//$('.ui.accordion').accordion('refresh');
|
||||
save_data();
|
||||
scroll_main_content();
|
||||
}
|
||||
|
||||
function callsign_select(callsign) {
|
||||
var tocall = $("#to_call");
|
||||
tocall.val(callsign);
|
||||
}
|
||||
|
||||
function reset_Tabs() {
|
||||
tabcontent = document.getElementsByClassName("tabcontent");
|
||||
for (i = 0; i < tabcontent.length; i++) {
|
||||
tabcontent[i].style.display = "none";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
function openCallsign(evt, callsign) {
|
||||
// This is called when a callsign tab is clicked
|
||||
var i, tabcontent, tablinks;
|
||||
|
||||
tab_content = tab_content_name(callsign);
|
||||
|
||||
tabcontent = document.getElementsByClassName("tabcontent");
|
||||
console.log(tabcontent);
|
||||
for (i = 0; i < tabcontent.length; i++) {
|
||||
tabcontent[i].style.display = "none";
|
||||
}
|
||||
tablinks = document.getElementsByClassName("tablinks");
|
||||
console.log(tablinks);
|
||||
for (i = 0; i < tablinks.length; i++) {
|
||||
tablinks[i].className = tablinks[i].className.replace(" active", "");
|
||||
}
|
||||
document.getElementById(tab_content).style.display = "block";
|
||||
evt.target.className += " active";
|
||||
callsign_select(callsign);
|
||||
var tocall = $("#to_call");
|
||||
tocall.val(callsign);
|
||||
scroll_main_content(callsign);
|
||||
selected_tab_callsign = callsign;
|
||||
tab_notify_id = tab_notification_id(callsign, true);
|
||||
$(tab_notify_id).addClass('visually-hidden');
|
||||
$(tab_notify_id).text(0);
|
||||
// Now update the path
|
||||
$('#pkt_path').val(callsign_list[callsign]);
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,374 @@
|
||||
// jQuery toast plugin created by Kamran Ahmed copyright MIT license 2015
|
||||
if ( typeof Object.create !== 'function' ) {
|
||||
Object.create = function( obj ) {
|
||||
function F() {}
|
||||
F.prototype = obj;
|
||||
return new F();
|
||||
};
|
||||
}
|
||||
|
||||
(function( $, window, document, undefined ) {
|
||||
|
||||
"use strict";
|
||||
|
||||
var Toast = {
|
||||
|
||||
_positionClasses : ['bottom-left', 'bottom-right', 'top-right', 'top-left', 'bottom-center', 'top-center', 'mid-center'],
|
||||
_defaultIcons : ['success', 'error', 'info', 'warning'],
|
||||
|
||||
init: function (options, elem) {
|
||||
this.prepareOptions(options, $.toast.options);
|
||||
this.process();
|
||||
},
|
||||
|
||||
prepareOptions: function(options, options_to_extend) {
|
||||
var _options = {};
|
||||
if ( ( typeof options === 'string' ) || ( options instanceof Array ) ) {
|
||||
_options.text = options;
|
||||
} else {
|
||||
_options = options;
|
||||
}
|
||||
this.options = $.extend( {}, options_to_extend, _options );
|
||||
},
|
||||
|
||||
process: function () {
|
||||
this.setup();
|
||||
this.addToDom();
|
||||
this.position();
|
||||
this.bindToast();
|
||||
this.animate();
|
||||
},
|
||||
|
||||
setup: function () {
|
||||
|
||||
var _toastContent = '';
|
||||
|
||||
this._toastEl = this._toastEl || $('<div></div>', {
|
||||
class : 'jq-toast-single'
|
||||
});
|
||||
|
||||
// For the loader on top
|
||||
_toastContent += '<span class="jq-toast-loader"></span>';
|
||||
|
||||
if ( this.options.allowToastClose ) {
|
||||
_toastContent += '<span class="close-jq-toast-single">×</span>';
|
||||
};
|
||||
|
||||
if ( this.options.text instanceof Array ) {
|
||||
|
||||
if ( this.options.heading ) {
|
||||
_toastContent +='<h2 class="jq-toast-heading">' + this.options.heading + '</h2>';
|
||||
};
|
||||
|
||||
_toastContent += '<ul class="jq-toast-ul">';
|
||||
for (var i = 0; i < this.options.text.length; i++) {
|
||||
_toastContent += '<li class="jq-toast-li" id="jq-toast-item-' + i + '">' + this.options.text[i] + '</li>';
|
||||
}
|
||||
_toastContent += '</ul>';
|
||||
|
||||
} else {
|
||||
if ( this.options.heading ) {
|
||||
_toastContent +='<h2 class="jq-toast-heading">' + this.options.heading + '</h2>';
|
||||
};
|
||||
_toastContent += this.options.text;
|
||||
}
|
||||
|
||||
this._toastEl.html( _toastContent );
|
||||
|
||||
if ( this.options.bgColor !== false ) {
|
||||
this._toastEl.css("background-color", this.options.bgColor);
|
||||
};
|
||||
|
||||
if ( this.options.textColor !== false ) {
|
||||
this._toastEl.css("color", this.options.textColor);
|
||||
};
|
||||
|
||||
if ( this.options.textAlign ) {
|
||||
this._toastEl.css('text-align', this.options.textAlign);
|
||||
}
|
||||
|
||||
if ( this.options.icon !== false ) {
|
||||
this._toastEl.addClass('jq-has-icon');
|
||||
|
||||
if ( $.inArray(this.options.icon, this._defaultIcons) !== -1 ) {
|
||||
this._toastEl.addClass('jq-icon-' + this.options.icon);
|
||||
};
|
||||
};
|
||||
|
||||
if ( this.options.class !== false ){
|
||||
this._toastEl.addClass(this.options.class)
|
||||
}
|
||||
},
|
||||
|
||||
position: function () {
|
||||
if ( ( typeof this.options.position === 'string' ) && ( $.inArray( this.options.position, this._positionClasses) !== -1 ) ) {
|
||||
|
||||
if ( this.options.position === 'bottom-center' ) {
|
||||
this._container.css({
|
||||
left: ( $(window).outerWidth() / 2 ) - this._container.outerWidth()/2,
|
||||
bottom: 20
|
||||
});
|
||||
} else if ( this.options.position === 'top-center' ) {
|
||||
this._container.css({
|
||||
left: ( $(window).outerWidth() / 2 ) - this._container.outerWidth()/2,
|
||||
top: 20
|
||||
});
|
||||
} else if ( this.options.position === 'mid-center' ) {
|
||||
this._container.css({
|
||||
left: ( $(window).outerWidth() / 2 ) - this._container.outerWidth()/2,
|
||||
top: ( $(window).outerHeight() / 2 ) - this._container.outerHeight()/2
|
||||
});
|
||||
} else {
|
||||
this._container.addClass( this.options.position );
|
||||
}
|
||||
|
||||
} else if ( typeof this.options.position === 'object' ) {
|
||||
this._container.css({
|
||||
top : this.options.position.top ? this.options.position.top : 'auto',
|
||||
bottom : this.options.position.bottom ? this.options.position.bottom : 'auto',
|
||||
left : this.options.position.left ? this.options.position.left : 'auto',
|
||||
right : this.options.position.right ? this.options.position.right : 'auto'
|
||||
});
|
||||
} else {
|
||||
this._container.addClass( 'bottom-left' );
|
||||
}
|
||||
},
|
||||
|
||||
bindToast: function () {
|
||||
|
||||
var that = this;
|
||||
|
||||
this._toastEl.on('afterShown', function () {
|
||||
that.processLoader();
|
||||
});
|
||||
|
||||
this._toastEl.find('.close-jq-toast-single').on('click', function ( e ) {
|
||||
|
||||
e.preventDefault();
|
||||
|
||||
if( that.options.showHideTransition === 'fade') {
|
||||
that._toastEl.trigger('beforeHide');
|
||||
that._toastEl.fadeOut(function () {
|
||||
that._toastEl.trigger('afterHidden');
|
||||
});
|
||||
} else if ( that.options.showHideTransition === 'slide' ) {
|
||||
that._toastEl.trigger('beforeHide');
|
||||
that._toastEl.slideUp(function () {
|
||||
that._toastEl.trigger('afterHidden');
|
||||
});
|
||||
} else {
|
||||
that._toastEl.trigger('beforeHide');
|
||||
that._toastEl.hide(function () {
|
||||
that._toastEl.trigger('afterHidden');
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
if ( typeof this.options.beforeShow == 'function' ) {
|
||||
this._toastEl.on('beforeShow', function () {
|
||||
that.options.beforeShow(that._toastEl);
|
||||
});
|
||||
};
|
||||
|
||||
if ( typeof this.options.afterShown == 'function' ) {
|
||||
this._toastEl.on('afterShown', function () {
|
||||
that.options.afterShown(that._toastEl);
|
||||
});
|
||||
};
|
||||
|
||||
if ( typeof this.options.beforeHide == 'function' ) {
|
||||
this._toastEl.on('beforeHide', function () {
|
||||
that.options.beforeHide(that._toastEl);
|
||||
});
|
||||
};
|
||||
|
||||
if ( typeof this.options.afterHidden == 'function' ) {
|
||||
this._toastEl.on('afterHidden', function () {
|
||||
that.options.afterHidden(that._toastEl);
|
||||
});
|
||||
};
|
||||
|
||||
if ( typeof this.options.onClick == 'function' ) {
|
||||
this._toastEl.on('click', function () {
|
||||
that.options.onClick(that._toastEl);
|
||||
});
|
||||
};
|
||||
},
|
||||
|
||||
addToDom: function () {
|
||||
|
||||
var _container = $('.jq-toast-wrap');
|
||||
|
||||
if ( _container.length === 0 ) {
|
||||
|
||||
_container = $('<div></div>',{
|
||||
class: "jq-toast-wrap",
|
||||
role: "alert",
|
||||
"aria-live": "polite"
|
||||
});
|
||||
|
||||
$('body').append( _container );
|
||||
|
||||
} else if ( !this.options.stack || isNaN( parseInt(this.options.stack, 10) ) ) {
|
||||
_container.empty();
|
||||
}
|
||||
|
||||
_container.find('.jq-toast-single:hidden').remove();
|
||||
|
||||
_container.append( this._toastEl );
|
||||
|
||||
if ( this.options.stack && !isNaN( parseInt( this.options.stack ), 10 ) ) {
|
||||
|
||||
var _prevToastCount = _container.find('.jq-toast-single').length,
|
||||
_extToastCount = _prevToastCount - this.options.stack;
|
||||
|
||||
if ( _extToastCount > 0 ) {
|
||||
$('.jq-toast-wrap').find('.jq-toast-single').slice(0, _extToastCount).remove();
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
this._container = _container;
|
||||
},
|
||||
|
||||
canAutoHide: function () {
|
||||
return ( this.options.hideAfter !== false ) && !isNaN( parseInt( this.options.hideAfter, 10 ) );
|
||||
},
|
||||
|
||||
processLoader: function () {
|
||||
// Show the loader only, if auto-hide is on and loader is demanded
|
||||
if (!this.canAutoHide() || this.options.loader === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
var loader = this._toastEl.find('.jq-toast-loader');
|
||||
|
||||
// 400 is the default time that jquery uses for fade/slide
|
||||
// Divide by 1000 for milliseconds to seconds conversion
|
||||
var transitionTime = (this.options.hideAfter - 400) / 1000 + 's';
|
||||
var loaderBg = this.options.loaderBg;
|
||||
|
||||
var style = loader.attr('style') || '';
|
||||
style = style.substring(0, style.indexOf('-webkit-transition')); // Remove the last transition definition
|
||||
|
||||
style += '-webkit-transition: width ' + transitionTime + ' ease-in; \
|
||||
-o-transition: width ' + transitionTime + ' ease-in; \
|
||||
transition: width ' + transitionTime + ' ease-in; \
|
||||
background-color: ' + loaderBg + ';';
|
||||
|
||||
|
||||
loader.attr('style', style).addClass('jq-toast-loaded');
|
||||
},
|
||||
|
||||
animate: function () {
|
||||
|
||||
var that = this;
|
||||
|
||||
this._toastEl.hide();
|
||||
|
||||
this._toastEl.trigger('beforeShow');
|
||||
|
||||
if ( this.options.showHideTransition.toLowerCase() === 'fade' ) {
|
||||
this._toastEl.fadeIn(function ( ){
|
||||
that._toastEl.trigger('afterShown');
|
||||
});
|
||||
} else if ( this.options.showHideTransition.toLowerCase() === 'slide' ) {
|
||||
this._toastEl.slideDown(function ( ){
|
||||
that._toastEl.trigger('afterShown');
|
||||
});
|
||||
} else {
|
||||
this._toastEl.show(function ( ){
|
||||
that._toastEl.trigger('afterShown');
|
||||
});
|
||||
}
|
||||
|
||||
if (this.canAutoHide()) {
|
||||
|
||||
var that = this;
|
||||
|
||||
window.setTimeout(function(){
|
||||
|
||||
if ( that.options.showHideTransition.toLowerCase() === 'fade' ) {
|
||||
that._toastEl.trigger('beforeHide');
|
||||
that._toastEl.fadeOut(function () {
|
||||
that._toastEl.trigger('afterHidden');
|
||||
});
|
||||
} else if ( that.options.showHideTransition.toLowerCase() === 'slide' ) {
|
||||
that._toastEl.trigger('beforeHide');
|
||||
that._toastEl.slideUp(function () {
|
||||
that._toastEl.trigger('afterHidden');
|
||||
});
|
||||
} else {
|
||||
that._toastEl.trigger('beforeHide');
|
||||
that._toastEl.hide(function () {
|
||||
that._toastEl.trigger('afterHidden');
|
||||
});
|
||||
}
|
||||
|
||||
}, this.options.hideAfter);
|
||||
};
|
||||
},
|
||||
|
||||
reset: function ( resetWhat ) {
|
||||
|
||||
if ( resetWhat === 'all' ) {
|
||||
$('.jq-toast-wrap').remove();
|
||||
} else {
|
||||
this._toastEl.remove();
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
update: function(options) {
|
||||
this.prepareOptions(options, this.options);
|
||||
this.setup();
|
||||
this.bindToast();
|
||||
},
|
||||
|
||||
close: function() {
|
||||
this._toastEl.find('.close-jq-toast-single').click();
|
||||
}
|
||||
};
|
||||
|
||||
$.toast = function(options) {
|
||||
var toast = Object.create(Toast);
|
||||
toast.init(options, this);
|
||||
|
||||
return {
|
||||
|
||||
reset: function ( what ) {
|
||||
toast.reset( what );
|
||||
},
|
||||
|
||||
update: function( options ) {
|
||||
toast.update( options );
|
||||
},
|
||||
|
||||
close: function( ) {
|
||||
toast.close( );
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
$.toast.options = {
|
||||
text: '',
|
||||
heading: '',
|
||||
showHideTransition: 'fade',
|
||||
allowToastClose: true,
|
||||
hideAfter: 3000,
|
||||
loader: true,
|
||||
loaderBg: '#9EC600',
|
||||
stack: 5,
|
||||
position: 'bottom-left',
|
||||
bgColor: false,
|
||||
textColor: false,
|
||||
textAlign: 'left',
|
||||
icon: false,
|
||||
beforeShow: function () {},
|
||||
afterShown: function () {},
|
||||
beforeHide: function () {},
|
||||
afterHidden: function () {},
|
||||
onClick: function () {}
|
||||
};
|
||||
|
||||
})( jQuery, window, document );
|
||||
@@ -1,106 +1,134 @@
|
||||
<html>
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1">
|
||||
<!-- <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script> -->
|
||||
<script src="/static/js/upstream/jquery.min.js"></script>
|
||||
<!-- <link rel="stylesheet" href="https://ajax.googleapis.com/ajax/libs/jqueryui/1.12.1/themes/smoothness/jquery-ui.css"> -->
|
||||
<link rel="stylesheet" href="/static/css/upstream/jquery-ui.css">
|
||||
<!-- <script src="https://ajax.googleapis.com/ajax/libs/jqueryui/1.12.1/jquery-ui.min.js"></script> -->
|
||||
<script src="/static/js/upstream/jquery-ui.min.js"></script>
|
||||
<!-- <script src="https://cdn.socket.io/4.1.2/socket.io.min.js" integrity="sha384-toS6mmwu70G0fw54EGlWWeA4z3dyJ+dlXBtSURSKN4vyRFOcxd3Bzjj/AoOwY+Rg" crossorigin="anonymous"></script> -->
|
||||
<script src="/static/js/upstream/socket.io.min.js"></script>
|
||||
<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="https://cdnjs.cloudflare.com/ajax/libs/fomantic-ui/2.9.0/semantic.min.css"> -->
|
||||
<link rel="stylesheet" href="/static/css/upstream/semantic.min.css">
|
||||
<!-- <script src="https://cdnjs.cloudflare.com/ajax/libs/fomantic-ui/2.9.0/semantic.min.js"></script> -->
|
||||
<script src="/static/js/upstream/semantic.min.js"></script>
|
||||
<link rel="stylesheet" href="/static/css/upstream/bootstrap.min.css">
|
||||
<script src="/static/js/upstream/bootstrap.bundle.min.js"></script>
|
||||
|
||||
<link rel="stylesheet" href="/static/css/index.css">
|
||||
<link rel="stylesheet" href="/static/css/tabs.css">
|
||||
<script src="/static/js/main.js"></script>
|
||||
<script src="/static/js/gps.js"></script>
|
||||
<script src="/static/js/send-message.js"></script>
|
||||
<!--
|
||||
<link 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>
|
||||
-->
|
||||
|
||||
<script type="text/javascript">
|
||||
var initial_stats = {{ initial_stats|tojson|safe }};
|
||||
var latitude = parseFloat('{{ latitude|safe }}');
|
||||
var longitude = parseFloat('{{ longitude|safe }}');
|
||||
<link rel="stylesheet" href="/static/css/upstream/google-fonts.css">
|
||||
<link rel="stylesheet" href="/static/css/upstream/jquery.toast.css">
|
||||
|
||||
var memory_chart = null
|
||||
var message_chart = null
|
||||
<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>
|
||||
|
||||
$(document).ready(function() {
|
||||
console.log(initial_stats);
|
||||
start_update();
|
||||
init_chat();
|
||||
reset_Tabs();
|
||||
<script type="text/javascript">
|
||||
var initial_stats = {{ initial_stats|tojson|safe }};
|
||||
var latitude = parseFloat('{{ latitude|safe }}');
|
||||
var longitude = parseFloat('{{ longitude|safe }}');
|
||||
|
||||
console.log("latitude", latitude);
|
||||
console.log("longitude", longitude);
|
||||
var memory_chart = null;
|
||||
var message_chart = null;
|
||||
|
||||
if (isNaN(latitude) || isNaN(longitude) && location.protocol != 'https:') {
|
||||
// Have to disable the beacon button.
|
||||
$('#send_beacon').prop('disabled', true);
|
||||
}
|
||||
$(document).ready(function() {
|
||||
console.log(initial_stats);
|
||||
start_update();
|
||||
init_chat();
|
||||
//reset_Tabs();
|
||||
|
||||
$("#wipe_local").click(function() {
|
||||
console.log('Wipe local storage');
|
||||
localStorage.clear();
|
||||
});
|
||||
console.log("latitude", latitude);
|
||||
console.log("longitude", longitude);
|
||||
|
||||
if (isNaN(latitude) || isNaN(longitude) && location.protocol != 'https:') {
|
||||
// Have to disable the beacon button.
|
||||
$('#send_beacon').prop('disabled', true);
|
||||
}
|
||||
|
||||
$("#wipe_local").click(function() {
|
||||
console.log('Wipe local storage');
|
||||
localStorage.clear();
|
||||
});
|
||||
</script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class='ui text container'>
|
||||
<h1 class='ui dividing header'>APRSD WebChat {{ version }}</h1>
|
||||
</div>
|
||||
// When a tab is clicked, populate the to_call form field.
|
||||
$(document).on('shown.bs.tab', 'button[data-bs-toggle="tab"]', function (e) {
|
||||
var tab = $(e.target);
|
||||
var callsign = tab.attr("callsign");
|
||||
var to_call = $('#to_call');
|
||||
to_call.val(callsign);
|
||||
selected_tab_callsign = callsign;
|
||||
});
|
||||
});
|
||||
|
||||
<div class='ui grid text container'>
|
||||
<div class='left floated ten wide column'>
|
||||
<span style='color: green'>{{ callsign }}</span>
|
||||
connected to
|
||||
<span style='color: blue' id='aprs_connection'>{{ aprs_connection|safe }}</span>
|
||||
</div>
|
||||
</script>
|
||||
</head>
|
||||
|
||||
<div class='right floated four wide column'>
|
||||
<span id='uptime'>NONE</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="ui container">
|
||||
<h3 class="ui dividing header">Send Message</h3>
|
||||
<div id="sendMsgDiv" class="ui mini text">
|
||||
<form id="sendform" name="sendmsg" action="">
|
||||
<div class="ui corner labeled input">
|
||||
<label for="to_call" class="ui label">Callsign</label>
|
||||
<input type="text" name="to_call" id="to_call" placeholder="To Callsign" size="11" maxlength="9">
|
||||
<div class="ui corner label">
|
||||
<i class="asterisk icon"></i>
|
||||
<body>
|
||||
<div class="wc-container">
|
||||
<div class="wc-row header">
|
||||
<div class="container-sm">
|
||||
<div class="row">
|
||||
<div class="column">
|
||||
<h1>APRSD WebChat {{ version }}</h1>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="column">
|
||||
<span style='color: green'>{{ callsign }}</span>
|
||||
connected to
|
||||
<span style='color: blue' id='aprs_connection'>{{ aprs_connection|safe }}</span>
|
||||
<span id='uptime'>NONE</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<form class="row gx-1 gy-1 justify-content-center align-items-center" id="sendform" name="sendmsg" action="" autocomplete="off">
|
||||
<div class="col-sm-2" style="width:150px;">
|
||||
<label for="to_call" class="visually-hidden">Callsign</label>
|
||||
<input type="search" class="form-control mb-2 mr-sm-2" name="to_call" id="to_call" placeholder="To Callsign" size="11" maxlength="9">
|
||||
</div>
|
||||
</div>
|
||||
<div class="ui labeled input">
|
||||
<label for="message" class="ui label">Message</label>
|
||||
<input type="text" name="message" id="message" size="40" maxlength="40">
|
||||
</div>
|
||||
<input type="submit" name="submit" class="ui button" id="send_msg" value="Send" />
|
||||
<button type="button" class="ui button" id="send_beacon" value="Send GPS Beacon">Send GPS Beacon</button>
|
||||
<button type="button" class="ui button" id="wipe_local" value="wipe local storage">Wipe LocalStorage</button>
|
||||
</form>
|
||||
<div class="col-auto">
|
||||
<label for="pkt_path" class="visually-hidden">PATH</label>
|
||||
<select class="form-control mb-2 mr-sm-2" name="pkt_path" id="pkt_path" style="width:auto;">
|
||||
<option value="" selected>Default Path</option>
|
||||
<option value="WIDE1-1">WIDE1-1</option>
|
||||
<option value="WIDE1-1,WIDE2-1">WIDE1-1,WIDE2-1</option>
|
||||
<option value="ARISS">ARISS</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-sm-3">
|
||||
<label for="message" class="visually-hidden">Message</label>
|
||||
<input type="search" class="form-control mb-2 mr-sm-2" name="message" id="message" size="40" maxlength="67" placeholder="Message">
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<input type="submit" name="submit" class="btn btn-primary mb-2" id="send_msg" value="Send"/>
|
||||
<button type="button" class="btn btn-primary mb-2" id="send_beacon" value="Send Position">Send Position</button>
|
||||
<!-- <button type="button" class="btn btn-primary mb-2" id="wipe_local" value="wipe local storage">Wipe LocalStorage</button> -->
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="container-sm" style="max-width: 800px">
|
||||
<ul class="nav nav-tabs" id="msgsTabList" role="tablist"></ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="ui grid">
|
||||
<div class="three wide column">
|
||||
<div class="tab" id="callsignTabs"></div>
|
||||
</div>
|
||||
<div class="ten wide column ui raised segment" id="msgsTabsDiv" style="height:450px;padding:0px;">
|
||||
<div class="wc-row content" id="wc-content">
|
||||
<div class="container" style="max-width: 800px;">
|
||||
<div class="tab-content" id="msgsTabContent">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="ui text container" style="padding-top: 40px">
|
||||
<a href="https://badge.fury.io/py/aprsd"><img src="https://badge.fury.io/py/aprsd.svg" alt="PyPI version" height="18"></a>
|
||||
<a href="https://github.com/craigerl/aprsd"><img src="https://img.shields.io/badge/Made%20with-Python-1f425f.svg" height="18"></a>
|
||||
<div class="wc-row footer">
|
||||
<div class="container-sm" style="padding-top: 40px">
|
||||
<a href="https://badge.fury.io/py/aprsd"><img src="https://badge.fury.io/py/aprsd.svg" alt="PyPI version" height="18"></a>
|
||||
<a href="https://github.com/craigerl/aprsd"><img src="https://img.shields.io/badge/Made%20with-Python-1f425f.svg" height="18"></a>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,98 +0,0 @@
|
||||
<html>
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1">
|
||||
<!-- <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script> -->
|
||||
<script src="/static/js/upstream/jquery.min.js"></script>
|
||||
<!-- <link rel="stylesheet" href="https://ajax.googleapis.com/ajax/libs/jqueryui/1.12.1/themes/smoothness/jquery-ui.css"> -->
|
||||
<link rel="stylesheet" href="/static/css/upstream/jquery-ui.css">
|
||||
<!-- <script src="https://ajax.googleapis.com/ajax/libs/jqueryui/1.12.1/jquery-ui.min.js"></script> -->
|
||||
<script src="/static/js/upstream/jquery-ui.min.js"></script>
|
||||
<!-- <script src="https://cdn.socket.io/4.1.2/socket.io.min.js" integrity="sha384-toS6mmwu70G0fw54EGlWWeA4z3dyJ+dlXBtSURSKN4vyRFOcxd3Bzjj/AoOwY+Rg" crossorigin="anonymous"></script> -->
|
||||
<script src="/static/js/upstream/socket.io.min.js"></script>
|
||||
|
||||
<!-- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/fomantic-ui/2.9.0/semantic.min.css"> -->
|
||||
<link rel="stylesheet" href="/static/css/upstream/semantic.min.css">
|
||||
<!-- <script src="https://cdnjs.cloudflare.com/ajax/libs/fomantic-ui/2.9.0/semantic.min.js"></script> -->
|
||||
<script src="/static/js/upstream/semantic.min.js"></script>
|
||||
|
||||
<link rel="stylesheet" href="/static/css/index.css">
|
||||
<script src="/static/js/main.js"></script>
|
||||
<script src="/static/js/gps.js"></script>
|
||||
<script src="/static/js/send-message-mobile.js"></script>
|
||||
|
||||
<script type="text/javascript">
|
||||
var initial_stats = {{ initial_stats|tojson|safe }};
|
||||
var 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();
|
||||
|
||||
console.log("latitude", latitude);
|
||||
console.log("longitude", longitude);
|
||||
|
||||
if (isNaN(latitude) || isNaN(longitude) && location.protocol != 'https:') {
|
||||
// Have to disable the beacon button.
|
||||
$('#send_beacon').prop('disabled', true);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class='ui text container'>
|
||||
<h1 class='ui dividing header'>APRSD WebChat {{ version }}</h1>
|
||||
</div>
|
||||
|
||||
<div class='ui grid text container' style="padding-bottom: 5px;">
|
||||
<div class='left floated twelve wide column'>
|
||||
<span style='color: green'>{{ callsign }}</span>
|
||||
connected to
|
||||
<span style='color: blue' id='aprs_connection'>{{ aprs_connection|safe }}</span>
|
||||
</div>
|
||||
|
||||
<div class='right floated four wide column'>
|
||||
<span id='uptime'>NONE</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="sendMsgDiv" class="ui grid" align="left" style="padding-top: 2px;">
|
||||
<h3 class="sixteen wide column ui dividing header">Send Message</h3>
|
||||
<form id="sendform" name="sendmsg" action="">
|
||||
<div class="sixteen wide column ui left labeled icon input">
|
||||
<div class="ui label">Callsign</div>
|
||||
<input type="text" name="to_call" id="to_call" placeholder="To Callsign" size="11" maxlength="9">
|
||||
<i class="users icon"></i>
|
||||
</div>
|
||||
<div class="sixteen wide column ui left labeled icon input" style="padding-bottom: 5px;">
|
||||
<label for="message" class="ui label">Message</label>
|
||||
<input type="text" name="message" id="message" maxlength="40" placeholder="Message">
|
||||
<i class="comment icon"></i>
|
||||
</div>
|
||||
<div class="right floated column">
|
||||
<input type="submit" name="submit" class="ui button" id="send_msg" value="Send" />
|
||||
<button type="button" class="ui button" id="send_beacon" value="Send GPS Beacon">Send GPS Beacon</button>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="ui grid">
|
||||
<div class="ui top attached tabular raised menu" id="callsignTabs">
|
||||
</div>
|
||||
<div class="sixteen wide column ui bottom attached raised tab segment" id="msgsTabsDiv" style="height:250px;padding:5px;">
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="ui text container" style="padding-top: 40px">
|
||||
<a href="https://badge.fury.io/py/aprsd"><img src="https://badge.fury.io/py/aprsd.svg" alt="PyPI version" height="18"></a>
|
||||
<a href="https://github.com/craigerl/aprsd"><img src="https://img.shields.io/badge/Made%20with-Python-1f425f.svg" height="18"></a>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
+53
-6
@@ -1,4 +1,6 @@
|
||||
import datetime
|
||||
import importlib.metadata as imp
|
||||
import io
|
||||
import json
|
||||
import logging
|
||||
from logging.handlers import RotatingFileHandler
|
||||
@@ -8,7 +10,7 @@ import flask
|
||||
from flask import Flask
|
||||
from flask.logging import default_handler
|
||||
from flask_httpauth import HTTPBasicAuth
|
||||
from oslo_config import cfg
|
||||
from oslo_config import cfg, generator
|
||||
import socketio
|
||||
from werkzeug.security import check_password_hash
|
||||
|
||||
@@ -93,12 +95,19 @@ def _stats():
|
||||
stats_dict["aprsd"]["watch_list"] = new_list
|
||||
packet_list = aprsd_rpc_client.RPCClient().get_packet_list()
|
||||
rx = tx = 0
|
||||
types = {}
|
||||
if packet_list:
|
||||
rx = packet_list.total_rx()
|
||||
tx = packet_list.total_tx()
|
||||
types_copy = packet_list.types.copy()
|
||||
|
||||
for key in types_copy:
|
||||
types[str(key)] = dict(types_copy[key])
|
||||
|
||||
stats_dict["packets"] = {
|
||||
"sent": tx,
|
||||
"received": rx,
|
||||
"types": types,
|
||||
}
|
||||
if track:
|
||||
size_tracker = len(track)
|
||||
@@ -123,7 +132,6 @@ def stats():
|
||||
@app.route("/")
|
||||
def index():
|
||||
stats = _stats()
|
||||
LOG.debug(stats)
|
||||
wl = aprsd_rpc_client.RPCClient().get_watch_list()
|
||||
if wl and wl.is_enabled():
|
||||
watch_count = len(wl)
|
||||
@@ -185,6 +193,7 @@ def index():
|
||||
watch_age=watch_age,
|
||||
seen_count=seen_count,
|
||||
plugin_count=plugin_count,
|
||||
# oslo_out=generate_oslo()
|
||||
)
|
||||
|
||||
|
||||
@@ -205,10 +214,12 @@ def get_packets():
|
||||
LOG.debug("/packets called")
|
||||
packet_list = aprsd_rpc_client.RPCClient().get_packet_list()
|
||||
if packet_list:
|
||||
packets = packet_list.get()
|
||||
tmp_list = []
|
||||
for pkt in packets:
|
||||
tmp_list.append(pkt.json)
|
||||
pkts = packet_list.copy()
|
||||
for key in pkts:
|
||||
pkt = packet_list.get(key)
|
||||
if pkt:
|
||||
tmp_list.append(pkt.json)
|
||||
|
||||
return json.dumps(tmp_list)
|
||||
else:
|
||||
@@ -225,6 +236,36 @@ def plugins():
|
||||
return "reloaded"
|
||||
|
||||
|
||||
def _get_namespaces():
|
||||
args = []
|
||||
|
||||
all = imp.entry_points()
|
||||
selected = []
|
||||
if "oslo.config.opts" in all:
|
||||
for x in all["oslo.config.opts"]:
|
||||
if x.group == "oslo.config.opts":
|
||||
selected.append(x)
|
||||
for entry in selected:
|
||||
if "aprsd" in entry.name:
|
||||
args.append("--namespace")
|
||||
args.append(entry.name)
|
||||
|
||||
return args
|
||||
|
||||
|
||||
def generate_oslo():
|
||||
CONF.namespace = _get_namespaces()
|
||||
string_out = io.StringIO()
|
||||
generator.generate(CONF, string_out)
|
||||
return string_out.getvalue()
|
||||
|
||||
|
||||
@auth.login_required
|
||||
@app.route("/oslo")
|
||||
def oslo():
|
||||
return generate_oslo()
|
||||
|
||||
|
||||
@auth.login_required
|
||||
@app.route("/save")
|
||||
def save():
|
||||
@@ -348,6 +389,8 @@ if __name__ == "uwsgi_file_aprsd_wsgi":
|
||||
log_level = init_app(
|
||||
log_level="DEBUG",
|
||||
config_file="/config/aprsd.conf",
|
||||
# Commented out for local development.
|
||||
# config_file=cli_helper.DEFAULT_CONFIG_FILE
|
||||
)
|
||||
setup_logging(app, log_level)
|
||||
sio.register_namespace(LoggingNamespace("/logs"))
|
||||
@@ -362,7 +405,11 @@ if __name__ == "aprsd.wsgi":
|
||||
sio = socketio.Server(logger=True, async_mode=async_mode)
|
||||
app.wsgi_app = socketio.WSGIApp(sio, app.wsgi_app)
|
||||
|
||||
log_level = init_app(config_file="/config/aprsd.conf", log_level="DEBUG")
|
||||
log_level = init_app(
|
||||
log_level="DEBUG",
|
||||
config_file="/config/aprsd.conf",
|
||||
# config_file=cli_helper.DEFAULT_CONFIG_FILE,
|
||||
)
|
||||
setup_logging(app, log_level)
|
||||
sio.register_namespace(LoggingNamespace("/logs"))
|
||||
CONF.log_opt_values(LOG, logging.DEBUG)
|
||||
|
||||
+32
-32
@@ -4,84 +4,84 @@
|
||||
#
|
||||
# pip-compile --annotation-style=line dev-requirements.in
|
||||
#
|
||||
add-trailing-comma==3.0.1 # via gray
|
||||
add-trailing-comma==3.1.0 # via gray
|
||||
alabaster==0.7.13 # via sphinx
|
||||
attrs==23.1.0 # via jsonschema, referencing
|
||||
autoflake==1.5.3 # via gray
|
||||
babel==2.12.1 # via sphinx
|
||||
black==23.7.0 # via gray
|
||||
build==0.10.0 # via pip-tools
|
||||
cachetools==5.3.1 # via tox
|
||||
babel==2.13.1 # via sphinx
|
||||
black==23.11.0 # via gray
|
||||
build==1.0.3 # via pip-tools
|
||||
cachetools==5.3.2 # via tox
|
||||
certifi==2023.7.22 # via requests
|
||||
cfgv==3.4.0 # via pre-commit
|
||||
chardet==5.2.0 # via tox
|
||||
charset-normalizer==3.2.0 # via requests
|
||||
click==8.1.6 # via black, pip-tools
|
||||
charset-normalizer==3.3.2 # via requests
|
||||
click==8.1.7 # via black, pip-tools
|
||||
colorama==0.4.6 # via tox
|
||||
commonmark==0.9.1 # via rich
|
||||
configargparse==1.7 # via gray
|
||||
coverage[toml]==7.3.0 # via pytest-cov
|
||||
coverage[toml]==7.3.2 # via coverage, pytest-cov
|
||||
distlib==0.3.7 # via virtualenv
|
||||
docutils==0.20.1 # via sphinx
|
||||
exceptiongroup==1.1.3 # via pytest
|
||||
filelock==3.12.2 # via tox, virtualenv
|
||||
filelock==3.13.1 # via tox, virtualenv
|
||||
fixit==0.1.4 # via gray
|
||||
flake8==6.1.0 # via -r dev-requirements.in, fixit, pep8-naming
|
||||
gray==0.13.0 # via -r dev-requirements.in
|
||||
identify==2.5.26 # via pre-commit
|
||||
identify==2.5.31 # via pre-commit
|
||||
idna==3.4 # via requests
|
||||
imagesize==1.4.1 # via sphinx
|
||||
importlib-resources==6.0.1 # via fixit
|
||||
importlib-resources==6.1.1 # via fixit
|
||||
iniconfig==2.0.0 # via pytest
|
||||
isort==5.12.0 # via -r dev-requirements.in, gray
|
||||
jinja2==3.1.2 # via sphinx
|
||||
jsonschema==4.19.0 # via fixit
|
||||
jsonschema-specifications==2023.7.1 # via jsonschema
|
||||
libcst==1.0.1 # via fixit
|
||||
jsonschema==4.20.0 # via fixit
|
||||
jsonschema-specifications==2023.11.1 # via jsonschema
|
||||
libcst==1.1.0 # via fixit
|
||||
markupsafe==2.1.3 # via jinja2
|
||||
mccabe==0.7.0 # via flake8
|
||||
mypy==1.5.0 # via -r dev-requirements.in
|
||||
mypy==1.7.0 # via -r dev-requirements.in
|
||||
mypy-extensions==1.0.0 # via black, mypy, typing-inspect
|
||||
nodeenv==1.8.0 # via pre-commit
|
||||
packaging==23.1 # via black, build, pyproject-api, pytest, sphinx, tox
|
||||
packaging==23.2 # via black, build, pyproject-api, pytest, sphinx, tox
|
||||
pathspec==0.11.2 # via black
|
||||
pep8-naming==0.13.3 # via -r dev-requirements.in
|
||||
pip-tools==7.3.0 # via -r dev-requirements.in
|
||||
platformdirs==3.10.0 # via black, tox, virtualenv
|
||||
pluggy==1.2.0 # via pytest, tox
|
||||
pre-commit==3.3.3 # via -r dev-requirements.in
|
||||
pycodestyle==2.11.0 # via flake8
|
||||
platformdirs==3.11.0 # via black, tox, virtualenv
|
||||
pluggy==1.3.0 # via pytest, tox
|
||||
pre-commit==3.5.0 # via -r dev-requirements.in
|
||||
pycodestyle==2.11.1 # via flake8
|
||||
pyflakes==3.1.0 # via autoflake, flake8
|
||||
pygments==2.16.1 # via rich, sphinx
|
||||
pyproject-api==1.5.3 # via tox
|
||||
pyproject-api==1.6.1 # via tox
|
||||
pyproject-hooks==1.0.0 # via build
|
||||
pytest==7.4.0 # via -r dev-requirements.in, pytest-cov
|
||||
pytest==7.4.3 # via -r dev-requirements.in, pytest-cov
|
||||
pytest-cov==4.1.0 # via -r dev-requirements.in
|
||||
pyupgrade==3.10.1 # via gray
|
||||
pyupgrade==3.15.0 # via gray
|
||||
pyyaml==6.0.1 # via fixit, libcst, pre-commit
|
||||
referencing==0.30.2 # via jsonschema, jsonschema-specifications
|
||||
referencing==0.31.0 # via jsonschema, jsonschema-specifications
|
||||
requests==2.31.0 # via sphinx
|
||||
rich==12.6.0 # via gray
|
||||
rpds-py==0.9.2 # via jsonschema, referencing
|
||||
rpds-py==0.13.0 # via jsonschema, referencing
|
||||
snowballstemmer==2.2.0 # via sphinx
|
||||
sphinx==7.1.2 # via -r dev-requirements.in, sphinxcontrib-applehelp, sphinxcontrib-devhelp, sphinxcontrib-htmlhelp, sphinxcontrib-qthelp, sphinxcontrib-serializinghtml
|
||||
sphinx==7.2.6 # via -r dev-requirements.in, sphinxcontrib-applehelp, sphinxcontrib-devhelp, sphinxcontrib-htmlhelp, sphinxcontrib-qthelp, sphinxcontrib-serializinghtml
|
||||
sphinxcontrib-applehelp==1.0.7 # via sphinx
|
||||
sphinxcontrib-devhelp==1.0.5 # via sphinx
|
||||
sphinxcontrib-htmlhelp==2.0.4 # via sphinx
|
||||
sphinxcontrib-jsmath==1.0.1 # via sphinx
|
||||
sphinxcontrib-qthelp==1.0.6 # via sphinx
|
||||
sphinxcontrib-serializinghtml==1.1.8 # via sphinx
|
||||
sphinxcontrib-serializinghtml==1.1.9 # via sphinx
|
||||
tokenize-rt==5.2.0 # via add-trailing-comma, pyupgrade
|
||||
toml==0.10.2 # via autoflake
|
||||
tomli==2.0.1 # via black, build, coverage, mypy, pip-tools, pyproject-api, pyproject-hooks, pytest, tox
|
||||
tox==4.8.0 # via -r dev-requirements.in
|
||||
typing-extensions==4.7.1 # via libcst, mypy, typing-inspect
|
||||
tox==4.11.3 # via -r dev-requirements.in
|
||||
typing-extensions==4.8.0 # via black, libcst, mypy, typing-inspect
|
||||
typing-inspect==0.9.0 # via libcst
|
||||
unify==0.5 # via gray
|
||||
untokenize==0.1.1 # via unify
|
||||
urllib3==2.0.4 # via requests
|
||||
virtualenv==20.24.3 # via pre-commit, tox
|
||||
wheel==0.41.1 # via pip-tools
|
||||
urllib3==2.1.0 # via requests
|
||||
virtualenv==20.24.6 # via pre-commit, tox
|
||||
wheel==0.41.3 # via pip-tools
|
||||
|
||||
# The following packages are considered to be unsafe in a requirements file:
|
||||
# pip
|
||||
|
||||
+4
-3
@@ -23,7 +23,7 @@ RUN set -ex \
|
||||
&& apt-get update \
|
||||
&& apt-get upgrade -y \
|
||||
&& apt-get install -y git build-essential curl libffi-dev \
|
||||
python3-dev libssl-dev libxml2-dev libxslt-dev telnet sudo \
|
||||
python3-dev libssl-dev libxml2-dev libxslt-dev telnet sudo fortune \
|
||||
# Install dependencies
|
||||
# Clean up
|
||||
&& apt-get autoremove -y \
|
||||
@@ -41,6 +41,7 @@ RUN mkdir /config
|
||||
RUN chown -R appuser:appgroup /app
|
||||
RUN chown -R appuser:appgroup /config
|
||||
USER appuser
|
||||
RUN echo "PATH=\$PATH:/usr/games" >> /app/.bashrc
|
||||
RUN which aprsd
|
||||
RUN aprsd sample-config > /config/aprsd.conf
|
||||
|
||||
@@ -57,5 +58,5 @@ VOLUME ["/config"]
|
||||
# Set the user to run the application
|
||||
USER appuser
|
||||
|
||||
HEALTHCHECK --interval=5m --timeout=12s --start-period=30s \
|
||||
CMD aprsd healthcheck --config /config/aprsd.conf
|
||||
HEALTHCHECK --interval=1m --timeout=12s --start-period=30s \
|
||||
CMD aprsd healthcheck --config /config/aprsd.conf --loglevel DEBUG
|
||||
|
||||
@@ -20,7 +20,7 @@ RUN set -ex \
|
||||
# Upgrade the package index and install security upgrades
|
||||
&& apt-get update \
|
||||
&& apt-get upgrade -y \
|
||||
&& apt-get install -y git build-essential curl libffi-dev \
|
||||
&& apt-get install -y git build-essential curl libffi-dev fortune \
|
||||
python3-dev libssl-dev libxml2-dev libxslt-dev telnet sudo \
|
||||
# Install dependencies
|
||||
# Clean up
|
||||
@@ -40,6 +40,7 @@ RUN mkdir /config
|
||||
RUN chown -R appuser:appgroup /app
|
||||
RUN chown -R appuser:appgroup /config
|
||||
USER appuser
|
||||
RUN echo "PATH=\$PATH:/usr/games" >> /app/.bashrc
|
||||
RUN which aprsd
|
||||
RUN aprsd sample-config > /config/aprsd.conf
|
||||
|
||||
|
||||
+1
-2
@@ -27,8 +27,6 @@ wrapt
|
||||
# kiss3 uses attrs
|
||||
kiss3
|
||||
attrs
|
||||
# for mobile checking
|
||||
user-agents
|
||||
dataclasses
|
||||
dacite2
|
||||
oslo.config
|
||||
@@ -38,3 +36,4 @@ rpyc
|
||||
shellingham
|
||||
geopy
|
||||
rush
|
||||
dataclasses-json
|
||||
|
||||
+35
-28
@@ -9,68 +9,75 @@ attrs==23.1.0 # via -r requirements.in, ax253, kiss3, rush
|
||||
ax253==0.1.5.post1 # via kiss3
|
||||
beautifulsoup4==4.12.2 # via -r requirements.in
|
||||
bidict==0.22.1 # via python-socketio
|
||||
bitarray==2.8.1 # via ax253, kiss3
|
||||
blinker==1.6.2 # via flask
|
||||
bitarray==2.8.3 # via ax253, kiss3
|
||||
blinker==1.7.0 # via flask
|
||||
certifi==2023.7.22 # via requests
|
||||
charset-normalizer==3.2.0 # via requests
|
||||
click==8.1.6 # via -r requirements.in, click-completion, click-params, flask
|
||||
charset-normalizer==3.3.2 # via requests
|
||||
click==8.1.7 # via -r requirements.in, click-completion, click-params, flask
|
||||
click-completion==0.5.2 # via -r requirements.in
|
||||
click-params==0.4.1 # via -r requirements.in
|
||||
commonmark==0.9.1 # via rich
|
||||
dacite2==2.0.0 # via -r requirements.in
|
||||
dataclasses==0.6 # via -r requirements.in
|
||||
dataclasses-json==0.6.2 # via -r requirements.in
|
||||
debtcollector==2.5.0 # via oslo-config
|
||||
decorator==5.1.1 # via validators
|
||||
dnspython==2.4.2 # via eventlet
|
||||
eventlet==0.33.3 # via -r requirements.in
|
||||
flask==2.3.2 # via -r requirements.in, flask-httpauth, flask-socketio
|
||||
flask==3.0.0 # via -r requirements.in, flask-httpauth, flask-socketio
|
||||
flask-httpauth==4.8.0 # via -r requirements.in
|
||||
flask-socketio==5.3.5 # via -r requirements.in
|
||||
flask-socketio==5.3.6 # via -r requirements.in
|
||||
geographiclib==2.0 # via geopy
|
||||
geopy==2.3.0 # via -r requirements.in
|
||||
gevent==23.7.0 # via -r requirements.in
|
||||
greenlet==2.0.2 # via eventlet, gevent
|
||||
geopy==2.4.0 # via -r requirements.in
|
||||
gevent==23.9.1 # via -r requirements.in
|
||||
greenlet==3.0.1 # via eventlet, gevent
|
||||
h11==0.14.0 # via wsproto
|
||||
idna==3.4 # via requests
|
||||
imapclient==2.3.1 # via -r requirements.in
|
||||
imapclient==3.0.0 # via -r requirements.in
|
||||
importlib-metadata==6.8.0 # via ax253, kiss3
|
||||
itsdangerous==2.1.2 # via flask
|
||||
jinja2==3.1.2 # via click-completion, flask
|
||||
kiss3==8.0.0 # via -r requirements.in
|
||||
markupsafe==2.1.3 # via jinja2, werkzeug
|
||||
netaddr==0.8.0 # via oslo-config
|
||||
oslo-config==9.1.1 # via -r requirements.in
|
||||
oslo-i18n==6.0.0 # via oslo-config
|
||||
pbr==5.11.1 # via -r requirements.in, oslo-i18n, stevedore
|
||||
pluggy==1.2.0 # via -r requirements.in
|
||||
marshmallow==3.20.1 # via dataclasses-json
|
||||
mypy-extensions==1.0.0 # via typing-inspect
|
||||
netaddr==0.9.0 # via oslo-config
|
||||
oslo-config==9.2.0 # via -r requirements.in
|
||||
oslo-i18n==6.2.0 # via oslo-config
|
||||
packaging==23.2 # via marshmallow
|
||||
pbr==6.0.0 # via -r requirements.in, oslo-i18n, stevedore
|
||||
pluggy==1.3.0 # via -r requirements.in
|
||||
plumbum==1.8.2 # via rpyc
|
||||
pygments==2.16.1 # via rich
|
||||
pyserial==3.5 # via pyserial-asyncio
|
||||
pyserial-asyncio==0.6 # via kiss3
|
||||
python-engineio==4.5.1 # via python-socketio
|
||||
python-socketio==5.8.0 # via -r requirements.in, flask-socketio
|
||||
pytz==2023.3 # via -r requirements.in
|
||||
python-engineio==4.8.0 # via python-socketio
|
||||
python-socketio==5.10.0 # via -r requirements.in, flask-socketio
|
||||
pytz==2023.3.post1 # via -r requirements.in
|
||||
pyyaml==6.0.1 # via -r requirements.in, oslo-config
|
||||
requests==2.31.0 # via -r requirements.in, oslo-config, update-checker
|
||||
rfc3986==2.0.0 # via oslo-config
|
||||
rich==12.6.0 # via -r requirements.in
|
||||
rpyc==5.3.1 # via -r requirements.in
|
||||
rush==2021.4.0 # via -r requirements.in
|
||||
shellingham==1.5.0.post1 # via -r requirements.in, click-completion
|
||||
six==1.16.0 # via -r requirements.in, click-completion, eventlet, imapclient
|
||||
soupsieve==2.4.1 # via beautifulsoup4
|
||||
shellingham==1.5.4 # via -r requirements.in, click-completion
|
||||
simple-websocket==1.0.0 # via python-engineio
|
||||
six==1.16.0 # via -r requirements.in, click-completion, eventlet
|
||||
soupsieve==2.5 # via beautifulsoup4
|
||||
stevedore==5.1.0 # via oslo-config
|
||||
tabulate==0.9.0 # via -r requirements.in
|
||||
thesmuggler==1.0.1 # via -r requirements.in
|
||||
ua-parser==0.18.0 # via user-agents
|
||||
typing-extensions==4.8.0 # via typing-inspect
|
||||
typing-inspect==0.9.0 # via dataclasses-json
|
||||
update-checker==0.18.0 # via -r requirements.in
|
||||
urllib3==2.0.4 # via requests
|
||||
user-agents==2.2.0 # via -r requirements.in
|
||||
urllib3==2.1.0 # via requests
|
||||
validators==0.20.0 # via click-params
|
||||
werkzeug==2.3.7 # via -r requirements.in, flask
|
||||
wrapt==1.15.0 # via -r requirements.in, debtcollector
|
||||
zipp==3.16.2 # via importlib-metadata
|
||||
werkzeug==3.0.1 # via -r requirements.in, flask
|
||||
wrapt==1.16.0 # via -r requirements.in, debtcollector
|
||||
wsproto==1.2.0 # via simple-websocket
|
||||
zipp==3.17.0 # via importlib-metadata
|
||||
zope-event==5.0 # via gevent
|
||||
zope-interface==6.0 # via gevent
|
||||
zope-interface==6.1 # via gevent
|
||||
|
||||
# The following packages are considered to be unsafe in a requirements file:
|
||||
# setuptools
|
||||
|
||||
Reference in New Issue
Block a user