diff --git a/aprsd/flask.py b/aprsd/flask.py index 2f0eee6..d69e34c 100644 --- a/aprsd/flask.py +++ b/aprsd/flask.py @@ -5,7 +5,7 @@ from logging import NullHandler from logging.handlers import RotatingFileHandler import sys -from aprsd import messaging, plugin, stats, utils +from aprsd import messaging, packets, plugin, stats, utils import flask import flask_classful from flask_httpauth import HTTPBasicAuth @@ -49,6 +49,7 @@ class APRSDFlask(flask_classful.FlaskView): "index.html", initial_stats=stats, callsign=self.config["aprs"]["login"], + config_json=json.dumps(self.config), ) @auth.login_required @@ -61,6 +62,11 @@ class APRSDFlask(flask_classful.FlaskView): return flask.render_template("messages.html", messages=json.dumps(msgs)) + @auth.login_required + def packets(self): + packet_list = packets.PacketList().packet_list + return json.dumps(packet_list) + @auth.login_required def plugins(self): pm = plugin.PluginManager() @@ -142,6 +148,7 @@ def init_flask(config, loglevel, quiet): flask_app.route("/", methods=["GET"])(server.index) flask_app.route("/stats", methods=["GET"])(server.stats) flask_app.route("/messages", methods=["GET"])(server.messages) + flask_app.route("/packets", methods=["GET"])(server.packets) flask_app.route("/save", methods=["GET"])(server.save) flask_app.route("/plugins", methods=["GET"])(server.plugins) return flask_app diff --git a/aprsd/messaging.py b/aprsd/messaging.py index e63a258..d340fc7 100644 --- a/aprsd/messaging.py +++ b/aprsd/messaging.py @@ -223,7 +223,7 @@ class Message(metaclass=abc.ABCMeta): id = 0 retry_count = 3 - last_send_time = None + last_send_time = 0 last_send_attempt = 0 def __init__(self, fromcall, tocall, msg_id=None): @@ -257,6 +257,9 @@ class RawMessage(Message): def dict(self): now = datetime.datetime.now() + last_send_age = None + if self.last_send_time: + last_send_age = str(now - self.last_send_time) return { "type": "raw", "message": self.message.rstrip("\n"), @@ -264,7 +267,7 @@ class RawMessage(Message): "retry_count": self.retry_count, "last_send_attempt": self.last_send_attempt, "last_send_time": str(self.last_send_time), - "last_send_age": str(now - self.last_send_time), + "last_send_age": last_send_age, } def __str__(self): @@ -304,6 +307,11 @@ class TextMessage(Message): def dict(self): now = datetime.datetime.now() + + last_send_age = None + if self.last_send_time: + last_send_age = str(now - self.last_send_time) + return { "id": self.id, "type": "text-message", @@ -314,7 +322,7 @@ class TextMessage(Message): "retry_count": self.retry_count, "last_send_attempt": self.last_send_attempt, "last_send_time": str(self.last_send_time), - "last_send_age": str(now - self.last_send_time), + "last_send_age": last_send_age, } def __str__(self): @@ -431,6 +439,9 @@ class AckMessage(Message): def dict(self): now = datetime.datetime.now() + last_send_age = None + if self.last_send_time: + last_send_age = str(now - self.last_send_time) return { "id": self.id, "type": "ack", @@ -440,7 +451,7 @@ class AckMessage(Message): "retry_count": self.retry_count, "last_send_attempt": self.last_send_attempt, "last_send_time": str(self.last_send_time), - "last_send_age": str(now - self.last_send_time), + "last_send_age": last_send_age, } def __str__(self): diff --git a/aprsd/packets.py b/aprsd/packets.py new file mode 100644 index 0000000..2c08dbb --- /dev/null +++ b/aprsd/packets.py @@ -0,0 +1,30 @@ +import logging +import threading +import time + +LOG = logging.getLogger("APRSD") + + +class PacketList: + """Class to track all of the packets rx'd and tx'd by aprsd.""" + + _instance = None + + packet_list = {} + + def __new__(cls, *args, **kwargs): + if cls._instance is None: + cls._instance = super().__new__(cls) + cls._instance.packet_list = {} + cls._instance.lock = threading.Lock() + return cls._instance + + def __iter__(self): + with self.lock: + return iter(self.packet_list) + + def add(self, packet): + with self.lock: + now = time.time() + ts = str(now).split(".")[0] + self.packet_list[ts] = packet diff --git a/aprsd/threads.py b/aprsd/threads.py index 5e95890..cea9deb 100644 --- a/aprsd/threads.py +++ b/aprsd/threads.py @@ -6,7 +6,7 @@ import threading import time import tracemalloc -from aprsd import client, messaging, plugin, stats, trace, utils +from aprsd import client, messaging, packets, plugin, stats, trace, utils import aprslib LOG = logging.getLogger("APRSD") @@ -77,6 +77,7 @@ class KeepAliveThread(APRSDThread): if self.cntr % 6 == 0: tracker = messaging.MsgTrack() stats_obj = stats.APRSDStats() + packets_list = packets.PacketList().packet_list now = datetime.datetime.now() last_email = stats_obj.email_thread_time if last_email: @@ -89,18 +90,16 @@ class KeepAliveThread(APRSDThread): current, peak = tracemalloc.get_traced_memory() stats_obj.set_memory(current) stats_obj.set_memory_peak(peak) - keepalive = ( - "Uptime {} Tracker {} " - "Msgs TX:{} RX:{} Last:{} Email:{} RAM Current:{} Peak:{}".format( - utils.strfdelta(stats_obj.uptime), - len(tracker), - stats_obj.msgs_tx, - stats_obj.msgs_rx, - last_msg_time, - email_thread_time, - utils.human_size(current), - utils.human_size(peak), - ) + keepalive = "Uptime {} Tracker {} " "Msgs TX:{} RX:{} Last:{} Email:{} Packets:{} RAM Current:{} Peak:{}".format( + utils.strfdelta(stats_obj.uptime), + len(tracker), + stats_obj.msgs_tx, + stats_obj.msgs_rx, + last_msg_time, + email_thread_time, + len(packets_list), + utils.human_size(current), + utils.human_size(peak), ) LOG.debug(keepalive) # Check version every hour @@ -244,6 +243,7 @@ class APRSDRXThread(APRSDThread): try: stats.APRSDStats().msgs_rx_inc() + packets.PacketList().add(packet) msg = packet.get("message_text", None) msg_format = packet.get("format", None) @@ -275,6 +275,7 @@ class APRSDTXThread(APRSDThread): def loop(self): try: msg = self.msg_queues["tx"].get(timeout=0.1) + packets.PacketList().add(msg.dict()) msg.send() except queue.Empty: pass diff --git a/aprsd/web/static/css/index.css b/aprsd/web/static/css/index.css new file mode 100644 index 0000000..64a30c3 --- /dev/null +++ b/aprsd/web/static/css/index.css @@ -0,0 +1,85 @@ +body { + display: grid; + grid-template-rows: auto 1fr auto; + background: #eeeeee; + margin: 2em; + padding: 0; + text-align: center; + font-family: system-ui, sans-serif; + height: 100vh; +} + +header { + padding: 2em; + height: 10vh; + margin: 10px; +} + +#main { + padding: 2em; + height: 80vh; +} +footer { + padding: 2em; + text-align: center; + height: 10vh; +} +#graphs { + display: grid; + width: 100%; + height: 300px; + grid-template-columns: 1fr 1fr; +} +#graphs_center { + display: block; + margin-top: 10px; + margin-bottom: 10px; + width: 100%; + height: 300px; +} +#left { + margin-right: 2px; + height: 300px; +} +#right { + height: 300px; +} +#center { + height: 300px; +} +#messageChart, #emailChart, #memChart { + border: 1px solid #ccc; + background: #ddd; +} +#stats { + margin: auto; + width: 80%; +} +#jsonstats { + display: none; +} +#title { + font-size: 4em; +} +#version{ + font-size: .5em; +} +#uptime, #aprsis { + font-size: 1em; +} +#callsign { + font-size: 1.4em; + color: #00F; + padding-top: 8px; + margin:10px; +} + +#title_rx { + background-color: darkseagreen; + text-align: left; +} + +#title_tx { + background-color: lightcoral; + text-align: left; +} diff --git a/aprsd/web/static/css/tabs.css b/aprsd/web/static/css/tabs.css new file mode 100644 index 0000000..b3a67a5 --- /dev/null +++ b/aprsd/web/static/css/tabs.css @@ -0,0 +1,35 @@ +/* Style the tab */ +.tab { + overflow: hidden; + border: 1px solid #ccc; + background-color: #f1f1f1; +} + +/* Style the buttons that are used to open the tab content */ +.tab button { + background-color: inherit; + float: left; + border: none; + outline: none; + cursor: pointer; + padding: 14px 16px; + transition: 0.3s; +} + +/* Change background color of buttons on hover */ +.tab button:hover { + background-color: #ddd; +} + +/* Create an active/current tablink class */ +.tab button.active { + background-color: #ccc; +} + +/* Style the tab content */ +.tabcontent { + display: none; + padding: 6px 12px; + border: 1px solid #ccc; + border-top: none; +} diff --git a/aprsd/web/static/js/charts.js b/aprsd/web/static/js/charts.js new file mode 100644 index 0000000..998b43d --- /dev/null +++ b/aprsd/web/static/js/charts.js @@ -0,0 +1,259 @@ +var packet_list = {}; + +window.chartColors = { + red: 'rgb(255, 99, 132)', + orange: 'rgb(255, 159, 64)', + yellow: 'rgb(255, 205, 86)', + green: 'rgb(26, 181, 77)', + blue: 'rgb(54, 162, 235)', + purple: 'rgb(153, 102, 255)', + grey: 'rgb(201, 203, 207)', + black: 'rgb(0, 0, 0)' +}; + +function start_charts() { + Chart.scaleService.updateScaleDefaults('linear', { + ticks: { + min: 0 + } + }); + + memory_chart = new Chart($("#memChart"), { + label: 'Memory Usage', + type: 'line', + data: { + labels: [], + datasets: [{ + label: 'Peak Ram usage', + borderColor: window.chartColors.red, + data: [], + }, + { + label: 'Current Ram usage', + borderColor: window.chartColors.blue, + data: [], + }], + }, + options: { + responsive: true, + maintainAspectRatio: false, + title: { + display: true, + text: 'Memory Usage', + }, + scales: { + x: { + type: 'timeseries', + offset: true, + ticks: { + major: { enabled: true }, + fontStyle: context => context.tick.major ? 'bold' : undefined, + source: 'data', + maxRotation: 0, + autoSkip: true, + autoSkipPadding: 75, + } + } + } + } + }); + + message_chart = new Chart($("#messageChart"), { + label: 'Messages', + type: 'line', + data: { + labels: [], + datasets: [{ + label: 'Messages Sent', + borderColor: window.chartColors.green, + data: [], + }, + { + label: 'Messages Recieved', + borderColor: window.chartColors.yellow, + data: [], + }, + { + label: 'Ack Sent', + borderColor: window.chartColors.purple, + data: [], + }, + { + label: 'Ack Recieved', + borderColor: window.chartColors.black, + data: [], + }], + }, + options: { + responsive: true, + maintainAspectRatio: false, + title: { + display: true, + text: 'APRS Messages', + }, + scales: { + x: { + type: 'timeseries', + offset: true, + ticks: { + major: { enabled: true }, + fontStyle: context => context.tick.major ? 'bold' : undefined, + source: 'data', + maxRotation: 0, + autoSkip: true, + autoSkipPadding: 75, + } + } + } + } + }); + + email_chart = new Chart($("#emailChart"), { + label: 'Email Messages', + type: 'line', + data: { + labels: [], + datasets: [{ + label: 'Sent', + borderColor: window.chartColors.green, + data: [], + }, + { + label: 'Recieved', + borderColor: window.chartColors.yellow, + data: [], + }], + }, + options: { + responsive: true, + maintainAspectRatio: false, + title: { + display: true, + text: 'Email Messages', + }, + scales: { + x: { + type: 'timeseries', + offset: true, + ticks: { + major: { enabled: true }, + fontStyle: context => context.tick.major ? 'bold' : undefined, + source: 'data', + maxRotation: 0, + autoSkip: true, + autoSkipPadding: 75, + } + } + } + } + }); +} + + +function addData(chart, label, newdata) { + chart.data.labels.push(label); + chart.data.datasets.forEach((dataset) => { + dataset.data.push(newdata); + }); + chart.update(); +} + +function updateDualData(chart, label, first, second) { + chart.data.labels.push(label); + chart.data.datasets[0].data.push(first); + chart.data.datasets[1].data.push(second); + chart.update(); +} +function updateQuadData(chart, label, first, second, third, fourth) { + chart.data.labels.push(label); + chart.data.datasets[0].data.push(first); + chart.data.datasets[1].data.push(second); + chart.data.datasets[2].data.push(third); + chart.data.datasets[3].data.push(fourth); + chart.update(); +} + +function update_stats( data ) { + $("#version").text( data["stats"]["aprsd"]["version"] ); + $("#aprsis").text( "APRS-IS Server: " + data["stats"]["aprs-is"]["server"] ); + $("#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); + short_time = data["time"].split(/\s(.+)/)[1]; + updateQuadData(message_chart, short_time, data["stats"]["messages"]["sent"], data["stats"]["messages"]["recieved"], 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"]); +} + + +function update_packets( data ) { + var packetsdiv = $("#packetsDiv"); + //nuke the contents first, then add to it. + jQuery.each(data, function(i, val) { + if ( packet_list.hasOwnProperty(i) == false ) { + packet_list[i] = val; + var d = new Date(i*1000).toLocaleDateString("en-US") + var t = new Date(i*1000).toLocaleTimeString("en-US") + if (val.hasOwnProperty('from') == false) { + from = val['fromcall'] + title_id = 'title_tx' + } else { + from = val['from'] + title_id = 'title_rx' + } + var from_to = d + " " + t + "    " + from + " > " + + if (val.hasOwnProperty('addresse')) { + from_to = from_to + val['addresse'] + } else if (val.hasOwnProperty('tocall')) { + from_to = from_to + val['tocall'] + } else if (val.hasOwnProperty('format') && val['format'] == 'mic-e') { + from_to = from_to + "Mic-E" + } + + from_to = from_to + "  -  " + val['raw'] + + json_pretty = Prism.highlight(JSON.stringify(val, null, '\t'), Prism.languages.json, 'json'); + pkt_html = '
' + from_to + '
' + json_pretty + '

' + packetsdiv.prepend(pkt_html); + } + }); + + $('.ui.accordion').accordion('refresh'); + + + const html_pretty = Prism.highlight(JSON.stringify(data, null, '\t'), Prism.languages.json, 'json'); + $("#packetsjson").html(html_pretty); +} + + +function start_update() { + + (function statsworker() { + $.ajax({ + url: "/stats", + type: 'GET', + dataType: 'json', + success: function(data) { + update_stats(data); + }, + complete: function() { + setTimeout(statsworker, 10000); + } + }); + })(); + + (function packetsworker() { + $.ajax({ + url: "/packets", + type: 'GET', + dataType: 'json', + success: function(data) { + update_packets(data); + }, + complete: function() { + setTimeout(packetsworker, 10000); + } + }); + })(); +} diff --git a/aprsd/web/static/js/tabs.js b/aprsd/web/static/js/tabs.js new file mode 100644 index 0000000..97cd2f8 --- /dev/null +++ b/aprsd/web/static/js/tabs.js @@ -0,0 +1,28 @@ +function openTab(evt, tabName) { + // Declare all variables + var i, tabcontent, tablinks; + + if (typeof tabName == 'undefined') { + return + } + + // Get all elements with class="tabcontent" and hide them + tabcontent = document.getElementsByClassName("tabcontent"); + for (i = 0; i < tabcontent.length; i++) { + tabcontent[i].style.display = "none"; + } + + // Get all elements with class="tablinks" and remove the class "active" + tablinks = document.getElementsByClassName("tablinks"); + for (i = 0; i < tablinks.length; i++) { + tablinks[i].className = tablinks[i].className.replace(" active", ""); + } + + // Show the current tab, and add an "active" class to the button that opened the tab + document.getElementById(tabName).style.display = "block"; + if (typeof evt.currentTarget == 'undefined') { + return + } else { + evt.currentTarget.className += " active"; + } +} diff --git a/aprsd/web/templates/index.html b/aprsd/web/templates/index.html index 382cde0..d0a7f7b 100644 --- a/aprsd/web/templates/index.html +++ b/aprsd/web/templates/index.html @@ -1,331 +1,95 @@ - - - + + + - - - - + + + + + + + + + + + + - - - - +
-
APRSD version
+
APRSD
{{ callsign }}
-
-
-
- -
-
-
+
+ + -
- -
{{ stats }}
+ +
+

Charts

+
+
+ +
+
+
+
+
+ +
{{ stats }}
+
+
+ +
+

Messages

+
+
+
+
+
+ +
+

Config

+
{{ config_json|safe }}