1
0
mirror of https://github.com/craigerl/aprsd.git synced 2025-09-02 13:17:54 -04:00

Merge pull request #101 from craigerl/webchat_mobile

Add support for mobile browsers for webchat
This commit is contained in:
Walter A. Boring IV 2022-12-02 17:06:17 -05:00 committed by GitHub
commit acecba27e8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 570 additions and 297 deletions

View File

@ -48,6 +48,7 @@ clean-test: ## remove test and coverage artifacts
clean-dev: clean-dev:
rm -rf $(VENVDIR) rm -rf $(VENVDIR)
rm Makefile.venv
test: dev ## Run all the tox tests test: dev ## Run all the tox tests
tox -p all tox -p all

View File

@ -2,15 +2,14 @@ import datetime
import json import json
import logging import logging
from logging.handlers import RotatingFileHandler from logging.handlers import RotatingFileHandler
import queue
import signal import signal
import sys import sys
import threading import threading
import time import time
import aprslib
from aprslib import util as aprslib_util from aprslib import util as aprslib_util
import click import click
from device_detector import DeviceDetector
import flask import flask
from flask import request from flask import request
from flask.logging import default_handler from flask.logging import default_handler
@ -26,7 +25,6 @@ from aprsd import config as aprsd_config
from aprsd import messaging, packets, stats, threads, utils from aprsd import messaging, packets, stats, threads, utils
from aprsd.aprsd import cli from aprsd.aprsd import cli
from aprsd.logging import rich as aprsd_logging from aprsd.logging import rich as aprsd_logging
from aprsd.threads import aprsd as aprsd_thread
from aprsd.threads import rx from aprsd.threads import rx
from aprsd.utils import objectstore, trace from aprsd.utils import objectstore, trace
@ -34,14 +32,6 @@ from aprsd.utils import objectstore, trace
LOG = logging.getLogger("APRSD") LOG = logging.getLogger("APRSD")
auth = HTTPBasicAuth() auth = HTTPBasicAuth()
users = None users = None
rx_msg_queue = queue.Queue(maxsize=20)
tx_msg_queue = queue.Queue(maxsize=20)
control_queue = queue.Queue(maxsize=20)
msg_queues = {
"rx": rx_msg_queue,
"control": control_queue,
"tx": tx_msg_queue,
}
def signal_handler(sig, frame): def signal_handler(sig, frame):
@ -142,62 +132,19 @@ def verify_password(username, password):
class WebChatRXThread(rx.APRSDRXThread): class WebChatRXThread(rx.APRSDRXThread):
"""Class that connects to aprsis/kiss and waits for messages.""" """Class that connects to APRISIS/kiss and waits for messages.
After the packet is received from APRSIS/KISS, the packet is
sent to processing in the WebChatProcessPacketThread.
"""
def __init__(self, config, socketio):
super().__init__(None, config)
self.socketio = socketio
self.connected = False
def connected(self, connected=True): def connected(self, connected=True):
self.connected = connected self.connected = connected
def stop(self):
self.thread_stop = True
client.factory.create().client.stop()
def loop(self):
# setup the consumer of messages and block until a messages
msg = None
try:
msg = self.msg_queues["tx"].get_nowait()
except queue.Empty:
pass
try:
if msg:
LOG.debug("GOT msg from TX queue!!")
msg.send()
except (
aprslib.exceptions.ConnectionDrop,
aprslib.exceptions.ConnectionError,
):
LOG.error("Connection dropped, reconnecting")
# Put it back on the queue to send.
self.msg_queues["tx"].put(msg)
# Force the deletion of the client object connected to aprs
# This will cause a reconnect, next time client.get_client()
# is called
self._client.reset()
time.sleep(2)
try:
# When new packets come in the consumer will process
# the packet
# This call blocks until thread stop() is called.
self._client.client.consumer(
self.process_packet, raw=False, blocking=False,
)
except (
aprslib.exceptions.ConnectionDrop,
aprslib.exceptions.ConnectionError,
):
LOG.error("Connection dropped, reconnecting")
time.sleep(5)
# Force the deletion of the client object connected to aprs
# This will cause a reconnect, next time client.get_client()
# is called
self._client.reset()
return True
return True
def process_packet(self, *args, **kwargs): def process_packet(self, *args, **kwargs):
# packet = self._client.decode_packet(*args, **kwargs) # packet = self._client.decode_packet(*args, **kwargs)
if "packet" in kwargs: if "packet" in kwargs:
@ -206,96 +153,55 @@ class WebChatRXThread(rx.APRSDRXThread):
packet = self._client.decode_packet(*args, **kwargs) packet = self._client.decode_packet(*args, **kwargs)
LOG.debug(f"GOT Packet {packet}") LOG.debug(f"GOT Packet {packet}")
self.msg_queues["rx"].put(packet) thread = WebChatProcessPacketThread(
config=self.config,
packet=packet,
socketio=self.socketio,
)
thread.start()
class WebChatTXThread(aprsd_thread.APRSDThread): class WebChatProcessPacketThread(rx.APRSDProcessPacketThread):
"""Class that """ """Class that handles packets being sent to us."""
def __init__(self, msg_queues, config, socketio): def __init__(self, config, packet, socketio):
super().__init__("_TXThread_")
self.msg_queues = msg_queues
self.config = config
self.socketio = socketio self.socketio = socketio
self.connected = False self.connected = False
super().__init__(config, packet)
def loop(self):
try:
msg = self.msg_queues["control"].get_nowait()
self.connected = msg["connected"]
except queue.Empty:
pass
try:
packet = self.msg_queues["rx"].get_nowait()
if packet:
# we got a packet and we need to send it to the
# web socket
self.process_packet(packet)
except queue.Empty:
pass
except Exception as ex:
LOG.exception(ex)
time.sleep(1)
return True
def process_ack_packet(self, packet): def process_ack_packet(self, packet):
super().process_ack_packet(packet)
ack_num = packet.get("msgNo") ack_num = packet.get("msgNo")
LOG.info(f"We got ack for our sent message {ack_num}")
messaging.log_packet(packet)
SentMessages().ack(int(ack_num)) SentMessages().ack(int(ack_num))
self.socketio.emit( self.socketio.emit(
"ack", SentMessages().get(int(ack_num)), "ack", SentMessages().get(int(ack_num)),
namespace="/sendmsg", namespace="/sendmsg",
) )
stats.APRSDStats().ack_rx_inc()
self.got_ack = True self.got_ack = True
def process_packet(self, packet): def process_non_ack_packet(self, packet):
LOG.info(f"process PACKET {packet}") LOG.info(f"process non ack PACKET {packet}")
tocall = packet.get("addresse", None) packet.get("addresse", None)
fromcall = packet["from"] fromcall = packet["from"]
msg = packet.get("message_text", None)
msg_id = packet.get("msgNo", "0")
msg_response = packet.get("response", None)
if tocall == self.config["aprsd"]["callsign"] and msg_response == "ack": packets.PacketList().add(packet)
self.process_ack_packet(packet) stats.APRSDStats().msgs_rx_inc()
elif tocall == self.config["aprsd"]["callsign"]: message = packet.get("message_text", None)
messaging.log_message( msg = {
"Received Message", "id": 0,
packet["raw"], "ts": time.time(),
msg, "ack": False,
fromcall=fromcall, "from": fromcall,
msg_num=msg_id, "to": packet["to"],
) "raw": packet["raw"],
# let any threads do their thing, then ack "message": message,
# send an ack last "status": None,
ack = messaging.AckMessage( "last_update": None,
self.config["aprsd"]["callsign"], "reply": None,
fromcall, }
msg_id=msg_id, self.socketio.emit(
) "new", msg,
ack.send() namespace="/sendmsg",
)
packets.PacketList().add(packet)
stats.APRSDStats().msgs_rx_inc()
message = packet.get("message_text", None)
msg = {
"id": 0,
"ts": time.time(),
"ack": False,
"from": fromcall,
"to": packet["to"],
"raw": packet["raw"],
"message": message,
"status": None,
"last_update": None,
"reply": None,
}
self.socketio.emit(
"new", msg,
namespace="/sendmsg",
)
class WebChatFlask(flask_classful.FlaskView): class WebChatFlask(flask_classful.FlaskView):
@ -312,10 +218,7 @@ class WebChatFlask(flask_classful.FlaskView):
users = self.users users = self.users
@auth.login_required def _get_transport(self, stats):
def index(self):
stats = self._stats()
if self.config["aprs"].get("enabled", True): if self.config["aprs"].get("enabled", True):
transport = "aprs-is" transport = "aprs-is"
aprs_connection = ( aprs_connection = (
@ -341,12 +244,35 @@ class WebChatFlask(flask_classful.FlaskView):
) )
) )
return transport, aprs_connection
@auth.login_required
def index(self):
user_agent = request.headers.get("User-Agent")
device = DeviceDetector(user_agent).parse()
LOG.debug(f"Device type {device.device_type()}")
LOG.debug(f"Is mobile? {device.is_mobile()}")
stats = self._stats()
if device.is_mobile():
html_template = "mobile.html"
else:
html_template = "index.html"
# For development
# html_template = "mobile.html"
LOG.debug(f"Template {html_template}")
transport, aprs_connection = self._get_transport(stats)
LOG.debug(f"transport {transport} aprs_connection {aprs_connection}")
stats["transport"] = transport stats["transport"] = transport
stats["aprs_connection"] = aprs_connection stats["aprs_connection"] = aprs_connection
LOG.debug(f"initial stats = {stats}") LOG.debug(f"initial stats = {stats}")
return flask.render_template( return flask.render_template(
"index.html", html_template,
initial_stats=stats, initial_stats=stats,
aprs_connection=aprs_connection, aprs_connection=aprs_connection,
callsign=self.config["aprsd"]["callsign"], callsign=self.config["aprsd"]["callsign"],
@ -392,9 +318,8 @@ class SendMessageNamespace(Namespace):
msg = None msg = None
request = None request = None
def __init__(self, namespace=None, config=None, msg_queues=None): def __init__(self, namespace=None, config=None):
self._config = config self._config = config
self._msg_queues = msg_queues
super().__init__(namespace) super().__init__(namespace)
def on_connect(self): def on_connect(self):
@ -404,13 +329,9 @@ class SendMessageNamespace(Namespace):
"connected", {"data": "/sendmsg Connected"}, "connected", {"data": "/sendmsg Connected"},
namespace="/sendmsg", namespace="/sendmsg",
) )
msg = {"connected": True}
self._msg_queues["control"].put(msg)
def on_disconnect(self): def on_disconnect(self):
LOG.debug("WS Disconnected") LOG.debug("WS Disconnected")
msg = {"connected": False}
self._msg_queues["control"].put(msg)
def on_send(self, data): def on_send(self, data):
global socketio global socketio
@ -419,7 +340,7 @@ class SendMessageNamespace(Namespace):
data["from"] = self._config["aprs"]["login"] data["from"] = self._config["aprs"]["login"]
msg = messaging.TextMessage( msg = messaging.TextMessage(
data["from"], data["from"],
data["to"], data["to"].upper(),
data["message"], data["message"],
) )
self.msg = msg self.msg = msg
@ -432,7 +353,6 @@ class SendMessageNamespace(Namespace):
namespace="/sendmsg", namespace="/sendmsg",
) )
msg.send() msg.send()
# self._msg_queues["tx"].put(msg)
def on_gps(self, data): def on_gps(self, data):
LOG.debug(f"WS on_GPS: {data}") LOG.debug(f"WS on_GPS: {data}")
@ -541,7 +461,6 @@ def init_flask(config, loglevel, quiet):
socketio.on_namespace( socketio.on_namespace(
SendMessageNamespace( SendMessageNamespace(
"/sendmsg", config=config, "/sendmsg", config=config,
msg_queues=msg_queues,
), ),
) )
return socketio, flask_app return socketio, flask_app
@ -618,18 +537,11 @@ def webchat(ctx, flush, port):
(socketio, app) = init_flask(config, loglevel, quiet) (socketio, app) = init_flask(config, loglevel, quiet)
rx_thread = WebChatRXThread( rx_thread = WebChatRXThread(
msg_queues=msg_queues,
config=config,
)
LOG.info("Start RX Thread")
rx_thread.start()
tx_thread = WebChatTXThread(
msg_queues=msg_queues,
config=config, config=config,
socketio=socketio, socketio=socketio,
) )
LOG.info("Start TX Thread") LOG.info("Start RX Thread")
tx_thread.start() rx_thread.start()
keepalive = threads.KeepAliveThread(config=config) keepalive = threads.KeepAliveThread(config=config)
LOG.info("Start KeepAliveThread") LOG.info("Start KeepAliveThread")

View File

@ -58,17 +58,32 @@ class APRSDRXThread(APRSDThread):
class APRSDPluginRXThread(APRSDRXThread): class APRSDPluginRXThread(APRSDRXThread):
"""Process received packets.
This is the main APRSD Server command thread that
receives packets from APRIS and then sends them for
processing in the PluginProcessPacketThread.
"""
def process_packet(self, *args, **kwargs): def process_packet(self, *args, **kwargs):
packet = self._client.decode_packet(*args, **kwargs) packet = self._client.decode_packet(*args, **kwargs)
thread = APRSDProcessPacketThread(packet=packet, config=self.config) thread = APRSDPluginProcessPacketThread(
config=self.config,
packet=packet,
)
thread.start() thread.start()
class APRSDProcessPacketThread(APRSDThread): class APRSDProcessPacketThread(APRSDThread):
"""Base class for processing received packets.
def __init__(self, packet, config): This is the base class for processing packets coming from
self.packet = packet the consumer. This base class handles sending ack packets and
will ack a message before sending the packet to the subclass
for processing."""
def __init__(self, config, packet):
self.config = config self.config = config
self.packet = packet
name = self.packet["raw"][:10] name = self.packet["raw"][:10]
super().__init__(f"RXPKT-{name}") super().__init__(f"RXPKT-{name}")
@ -88,7 +103,7 @@ class APRSDProcessPacketThread(APRSDThread):
return return
def loop(self): def loop(self):
"""Process a packet recieved from aprs-is server.""" """Process a packet received from aprs-is server."""
packet = self.packet packet = self.packet
packets.PacketList().add(packet) packets.PacketList().add(packet)
@ -101,7 +116,11 @@ class APRSDProcessPacketThread(APRSDThread):
# We don't put ack packets destined for us through the # We don't put ack packets destined for us through the
# plugins. # plugins.
if tocall == self.config["aprsd"]["callsign"] and msg_response == "ack": if (
tocall
and tocall.lower() == self.config["aprsd"]["callsign"].lower()
and msg_response == "ack"
):
self.process_ack_packet(packet) self.process_ack_packet(packet)
else: else:
# It's not an ACK for us, so lets run it through # It's not an ACK for us, so lets run it through
@ -115,7 +134,10 @@ class APRSDProcessPacketThread(APRSDThread):
) )
# Only ack messages that were sent directly to us # Only ack messages that were sent directly to us
if (tocall.lower() == self.config["aprsd"]["callsign"].lower()): if (
tocall
and tocall.lower() == self.config["aprsd"]["callsign"].lower()
):
stats.APRSDStats().msgs_rx_inc() stats.APRSDStats().msgs_rx_inc()
# let any threads do their thing, then ack # let any threads do their thing, then ack
# send an ack last # send an ack last
@ -126,69 +148,89 @@ class APRSDProcessPacketThread(APRSDThread):
) )
ack.send() ack.send()
pm = plugin.PluginManager() self.process_non_ack_packet(packet)
try: else:
results = pm.run(packet) LOG.info("Packet was not for us.")
wl = packets.WatchList() LOG.debug("Packet processing complete")
wl.update_seen(packet)
replied = False
for reply in results:
if isinstance(reply, list):
# one of the plugins wants to send multiple messages
replied = True
for subreply in reply:
LOG.debug(f"Sending '{subreply}'")
if isinstance(subreply, messaging.Message):
subreply.send()
else:
msg = messaging.TextMessage(
self.config["aprsd"]["callsign"],
fromcall,
subreply,
)
msg.send()
elif isinstance(reply, messaging.Message):
# We have a message based object.
LOG.debug(f"Sending '{reply}'")
reply.send()
replied = True
else:
replied = True
# A plugin can return a null message flag which signals
# us that they processed the message correctly, but have
# nothing to reply with, so we avoid replying with a
# usage string
if reply is not messaging.NULL_MESSAGE:
LOG.debug(f"Sending '{reply}'")
@abc.abstractmethod
def process_non_ack_packet(self, *args, **kwargs):
"""Ack packets already dealt with here."""
class APRSDPluginProcessPacketThread(APRSDProcessPacketThread):
"""Process the packet through the plugin manager.
This is the main aprsd server plugin processing thread."""
def process_non_ack_packet(self, packet):
"""Send the packet through the plugins."""
fromcall = packet["from"]
tocall = packet.get("addresse", None)
msg = packet.get("message_text", None)
packet.get("msgNo", "0")
packet.get("response", None)
pm = plugin.PluginManager()
try:
results = pm.run(packet)
wl = packets.WatchList()
wl.update_seen(packet)
replied = False
for reply in results:
if isinstance(reply, list):
# one of the plugins wants to send multiple messages
replied = True
for subreply in reply:
LOG.debug(f"Sending '{subreply}'")
if isinstance(subreply, messaging.Message):
subreply.send()
else:
msg = messaging.TextMessage( msg = messaging.TextMessage(
self.config["aprsd"]["callsign"], self.config["aprsd"]["callsign"],
fromcall, fromcall,
reply, subreply,
) )
msg.send() msg.send()
elif isinstance(reply, messaging.Message):
# We have a message based object.
LOG.debug(f"Sending '{reply}'")
reply.send()
replied = True
else:
replied = True
# A plugin can return a null message flag which signals
# us that they processed the message correctly, but have
# nothing to reply with, so we avoid replying with a
# usage string
if reply is not messaging.NULL_MESSAGE:
LOG.debug(f"Sending '{reply}'")
# If the message was for us and we didn't have a msg = messaging.TextMessage(
# response, then we send a usage statement. self.config["aprsd"]["callsign"],
if tocall == self.config["aprsd"]["callsign"] and not replied: fromcall,
LOG.warning("Sending help!") reply,
msg = messaging.TextMessage( )
self.config["aprsd"]["callsign"], msg.send()
fromcall,
"Unknown command! Send 'help' message for help",
)
msg.send()
except Exception as ex:
LOG.error("Plugin failed!!!")
LOG.exception(ex)
# Do we need to send a reply?
if tocall == self.config["aprsd"]["callsign"]:
reply = "A Plugin failed! try again?"
msg = messaging.TextMessage(
self.config["aprsd"]["callsign"],
fromcall,
reply,
)
msg.send()
LOG.debug("Packet processing complete") # If the message was for us and we didn't have a
# response, then we send a usage statement.
if tocall == self.config["aprsd"]["callsign"] and not replied:
LOG.warning("Sending help!")
msg = messaging.TextMessage(
self.config["aprsd"]["callsign"],
fromcall,
"Unknown command! Send 'help' message for help",
)
msg.send()
except Exception as ex:
LOG.error("Plugin failed!!!")
LOG.exception(ex)
# Do we need to send a reply?
if tocall == self.config["aprsd"]["callsign"]:
reply = "A Plugin failed! try again?"
msg = messaging.TextMessage(
self.config["aprsd"]["callsign"],
fromcall,
reply,
)
msg.send()

View File

@ -0,0 +1,63 @@
function init_gps() {
console.log("init_gps Called.")
$("#send_beacon").click(function() {
console.log("Send a beacon!")
getLocation();
});
}
function getLocation() {
if (navigator.geolocation) {
console.log("getCurrentPosition");
navigator.geolocation.getCurrentPosition(showPosition, showError);
} else {
var msg = "Geolocation is not supported by this browser."
console.log(msg);
alert(msg)
}
}
function showError(error) {
console.log("showError");
console.log(error);
var msg = "";
switch(error.code) {
case error.PERMISSION_DENIED:
msg = "User denied the request for Geolocation."
break;
case error.POSITION_UNAVAILABLE:
msg = "Location information is unavailable."
break;
case error.TIMEOUT:
msg = "The request to get user location timed out."
break;
case error.UNKNOWN_ERROR:
msg = "An unknown error occurred."
break;
}
console.log(msg);
$.toast({
title: 'GPS Error',
message: msg,
showProgress: 'bottom',
classProgress: 'red'
});
}
function showPosition(position) {
console.log("showPosition Called");
msg = {
'latitude': position.coords.latitude,
'longitude': position.coords.longitude
}
console.log(msg);
$.toast({
title: 'Sending GPS Beacon',
message: "Latitude: "+position.coords.latitude+"<br>Longitude: "+position.coords.longitude,
showProgress: 'bottom',
classProgress: 'red'
});
socket.emit("gps", msg);
}

View File

@ -0,0 +1,223 @@
var cleared = false;
var callsign_list = {};
var message_list = {};
function size_dict(d){c=0; for (i in d) ++c; return c}
function init_chat() {
const socket = io("/sendmsg");
socket.on('connect', function () {
console.log("Connected to socketio");
});
socket.on('connected', function(msg) {
console.log("Connected!");
console.log(msg);
});
socket.on("sent", function(msg) {
if (cleared == false) {
var msgsdiv = $("#msgsTabsDiv");
msgsdiv.html('')
cleared = true
}
sent_msg(msg);
});
socket.on("ack", function(msg) {
update_msg(msg);
});
socket.on("new", function(msg) {
if (cleared == false) {
var msgsdiv = $("#msgsTabsDiv");
msgsdiv.html('')
cleared = true
}
from_msg(msg);
});
$("#sendform").submit(function(event) {
event.preventDefault();
msg = {'to': $('#to_call').val().toUpperCase(),
'message': $('#message').val(),
}
socket.emit("send", msg);
$('#message').val('');
$('#to_call').val('');
});
init_gps();
}
function add_callsign(callsign) {
/* Ensure a callsign exists in the left hand nav */
dropdown = $('#callsign_dropdown')
if (callsign in callsign_list) {
console.log(callsign+' already in list.')
return false
}
var callsignTabs = $("#callsignTabs");
tab_name = tab_string(callsign);
tab_content = tab_content_name(callsign);
divname = content_divname(callsign);
item_html = '<div class="active item" id="'+tab_name+'" onclick="openCallsign(event, \''+callsign+'\');">'+callsign+'</div>';
callsignTabs.append(item_html);
callsign_list[callsign] = {'name': callsign, 'value': callsign, 'text': callsign}
return true
}
function append_message(callsign, msg, msg_html) {
console.log('append_message');
new_callsign = false
if (!message_list.hasOwnProperty(callsign)) {
message_list[callsign] = new Array();
}
message_list[callsign].push(msg);
// Find the right div to place the html
new_callsign = add_callsign(callsign);
append_message_html(callsign, msg_html, new_callsign);
if (new_callsign) {
//click on the new tab
click_div = '#'+tab_string(callsign);
console.log("Click on "+click_div);
$(click_div).click();
}
}
function tab_string(callsign) {
return "msgs"+callsign;
}
function tab_content_name(callsign) {
return tab_string(callsign)+"Content";
}
function content_divname(callsign) {
return "#"+tab_content_name(callsign);
}
function append_message_html(callsign, msg_html, new_callsign) {
var msgsTabs = $('#msgsTabsDiv');
divname_str = tab_content_name(callsign);
divname = content_divname(callsign);
if (new_callsign) {
// we have to add a new DIV
msg_div_html = '<div class="tabcontent" id="'+divname_str+'" style="height:450px;">'+msg_html+'</div>';
msgsTabs.append(msg_div_html);
} else {
var msgDiv = $(divname);
msgDiv.append(msg_html);
}
$(divname).animate({scrollTop: $(divname)[0].scrollHeight}, "slow");
}
function create_message_html(time, from, to, message, ack) {
msg_html = '<div class="item">';
msg_html += '<div class="tiny text">'+time+'</div>';
msg_html += '<div class="middle aligned content">';
msg_html += '<div class="tiny red header">'+from+'</div>';
if (ack) {
msg_html += '<i class="thumbs down outline icon" id="' + ack_id + '" data-content="Waiting for ACK"></i>';
} else {
msg_html += '<i class="phone volume icon" data-content="Recieved Message"></i>';
}
msg_html += '<div class="middle aligned content">>&nbsp;&nbsp;&nbsp;</div>';
msg_html += '</div>';
msg_html += '<div class="middle aligned content">'+message+'</div>';
msg_html += '</div><br>';
return msg_html
}
function sent_msg(msg) {
var msgsdiv = $("#sendMsgsDiv");
ts_str = msg["ts"].toString();
ts = ts_str.split(".")[0]*1000;
id = ts_str.split('.')[0]
ack_id = "ack_" + id
var d = new Date(ts).toLocaleDateString("en-US")
var t = new Date(ts).toLocaleTimeString("en-US")
msg_html = create_message_html(t, msg['from'], msg['to'], msg['message'], ack_id);
append_message(msg['to'], msg, msg_html);
}
function from_msg(msg) {
var msgsdiv = $("#sendMsgsDiv");
// We have an existing entry
ts_str = msg["ts"].toString();
ts = ts_str.split(".")[0]*1000;
id = ts_str.split('.')[0]
ack_id = "ack_" + id
var d = new Date(ts).toLocaleDateString("en-US")
var t = new Date(ts).toLocaleTimeString("en-US")
from = msg['from']
msg_html = create_message_html(t, from, false, msg['message'], false);
append_message(from, msg, msg_html);
}
function update_msg(msg) {
var msgsdiv = $("#sendMsgsDiv");
// We have an existing entry
ts_str = msg["ts"].toString();
id = ts_str.split('.')[0]
pretty_id = "pretty_" + id
loader_id = "loader_" + id
ack_id = "ack_" + id
span_id = "span_" + id
if (msg['ack'] == true) {
var loader_div = $('#' + loader_id);
var ack_div = $('#' + ack_id);
loader_div.removeClass('ui active inline loader');
loader_div.addClass('ui disabled loader');
ack_div.removeClass('thumbs up outline icon');
ack_div.addClass('thumbs up outline icon');
}
$('.ui.accordion').accordion('refresh');
}
function callsign_select(callsign) {
var tocall = $("#to_call");
tocall.val(callsign);
}
function reset_Tabs() {
tabcontent = document.getElementsByClassName("tabcontent");
for (i = 0; i < tabcontent.length; i++) {
tabcontent[i].style.display = "none";
}
}
function openCallsign(evt, callsign) {
var i, tabcontent, tablinks;
tab_content = tab_content_name(callsign);
tabcontent = document.getElementsByClassName("tabcontent");
for (i = 0; i < tabcontent.length; i++) {
tabcontent[i].style.display = "none";
}
tablinks = document.getElementsByClassName("tablinks");
for (i = 0; i < tablinks.length; i++) {
tablinks[i].className = tablinks[i].className.replace(" active", "");
}
document.getElementById(tab_content).style.display = "block";
evt.target.className += " active";
callsign_select(callsign);
}

View File

@ -45,56 +45,9 @@ function init_chat() {
$('#message').val(''); $('#message').val('');
}); });
$("#send_beacon").click(function() { init_gps();
console.log("Send a beacon!")
getLocation();
});
} }
function getLocation() {
if (navigator.geolocation) {
console.log("getCurrentPosition");
navigator.geolocation.getCurrentPosition(showPosition, showError);
} else {
var msg = "Geolocation is not supported by this browser."
console.log(msg);
alert(msg)
}
}
function showError(error) {
console.log("showError");
console.log(error);
var msg = "";
switch(error.code) {
case error.PERMISSION_DENIED:
msg = "User denied the request for Geolocation."
break;
case error.POSITION_UNAVAILABLE:
msg = "Location information is unavailable."
break;
case error.TIMEOUT:
msg = "The request to get user location timed out."
break;
case error.UNKNOWN_ERROR:
msg = "An unknown error occurred."
break;
}
console.log(msg);
alert(msg);
}
function showPosition(position) {
console.log("showPosition Called");
msg = {
'latitude': position.coords.latitude,
'longitude': position.coords.longitude
}
console.log(msg);
socket.emit("gps", msg);
}
function add_callsign(callsign) { function add_callsign(callsign) {
/* Ensure a callsign exists in the left hand nav */ /* Ensure a callsign exists in the left hand nav */

View File

@ -6,15 +6,15 @@
<script src="https://ajax.googleapis.com/ajax/libs/jqueryui/1.12.1/jquery-ui.min.js"></script> <script src="https://ajax.googleapis.com/ajax/libs/jqueryui/1.12.1/jquery-ui.min.js"></script>
<script src="https://cdn.socket.io/4.1.2/socket.io.min.js" integrity="sha384-toS6mmwu70G0fw54EGlWWeA4z3dyJ+dlXBtSURSKN4vyRFOcxd3Bzjj/AoOwY+Rg" crossorigin="anonymous"></script> <script src="https://cdn.socket.io/4.1.2/socket.io.min.js" integrity="sha384-toS6mmwu70G0fw54EGlWWeA4z3dyJ+dlXBtSURSKN4vyRFOcxd3Bzjj/AoOwY+Rg" crossorigin="anonymous"></script>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/semantic-ui@2.4.2/dist/semantic.min.css"> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/fomantic-ui/2.9.0/semantic.min.css">
<script src="https://cdn.jsdelivr.net/npm/semantic-ui@2.4.2/dist/semantic.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/fomantic-ui/2.9.0/semantic.min.js"></script>
<link rel="stylesheet" href="/static/css/index.css"> <link rel="stylesheet" href="/static/css/index.css">
<link rel="stylesheet" href="/static/css/tabs.css"> <link rel="stylesheet" href="/static/css/tabs.css">
<script src="/static/js/main.js"></script> <script src="/static/js/main.js"></script>
<script src="/static/js/gps.js"></script>
<script src="/static/js/send-message.js"></script> <script src="/static/js/send-message.js"></script>
<script type="text/javascript"> <script type="text/javascript">
var initial_stats = {{ initial_stats|tojson|safe }}; var initial_stats = {{ initial_stats|tojson|safe }};
@ -66,16 +66,14 @@
<button type="button" class="ui button" id="send_beacon" value="Send GPS Beacon">Send GPS Beacon</button> <button type="button" class="ui button" id="send_beacon" value="Send GPS Beacon">Send GPS Beacon</button>
</form> </form>
</div> </div>
</div> </div>
<div class="ui grid"> <div class="ui grid">
<div class="three wide column"> <div class="three wide column">
<div class="tab" id="callsignTabs"> <div class="tab" id="callsignTabs"></div>
</div> </div>
</div> <div class="ten wide column ui raised segment" id="msgsTabsDiv" style="height:450px;padding:0px;">
<div class="ten wide column ui raised segment" id="msgsTabsDiv" style="height:450px;padding:0px;"> &nbsp;
&nbsp;
</div>
</div> </div>
</div> </div>

View File

@ -0,0 +1,82 @@
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1">
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.1/jquery.min.js"></script>
<link rel="stylesheet" href="https://ajax.googleapis.com/ajax/libs/jqueryui/1.12.1/themes/smoothness/jquery-ui.css">
<script src="https://ajax.googleapis.com/ajax/libs/jqueryui/1.12.1/jquery-ui.min.js"></script>
<script src="https://cdn.socket.io/4.1.2/socket.io.min.js" integrity="sha384-toS6mmwu70G0fw54EGlWWeA4z3dyJ+dlXBtSURSKN4vyRFOcxd3Bzjj/AoOwY+Rg" crossorigin="anonymous"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/fomantic-ui/2.9.0/semantic.min.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/fomantic-ui/2.9.0/semantic.min.js"></script>
<link rel="stylesheet" href="/static/css/index.css">
<script src="/static/js/main.js"></script>
<script src="/static/js/gps.js"></script>
<script src="/static/js/send-message-mobile.js"></script>
<script type="text/javascript">
var initial_stats = {{ initial_stats|tojson|safe }};
var memory_chart = null
var message_chart = null
$(document).ready(function() {
console.log(initial_stats);
start_update();
init_chat();
});
</script>
</head>
<body>
<div class='ui text container'>
<h1 class='ui dividing header'>APRSD WebChat {{ version }}</h1>
</div>
<div class='ui grid text container' style="padding-bottom: 5px;">
<div class='left floated twelve wide column'>
<span style='color: green'>{{ callsign }}</span>
connected to
<span style='color: blue' id='aprs_connection'>{{ aprs_connection|safe }}</span>
</div>
<div class='right floated four wide column'>
<span id='uptime'>NONE</span>
</div>
</div>
<div id="sendMsgDiv" class="ui grid" align="left" style="padding-top: 2px;">
<h3 class="sixteen wide column ui dividing header">Send Message</h3>
<form id="sendform" name="sendmsg" action="">
<div class="sixteen wide column ui left labeled icon input">
<div class="ui label">Callsign</div>
<input type="text" name="to_call" id="to_call" placeholder="To Callsign" size="11" maxlength="9">
<i class="users icon"></i>
</div>
<div class="sixteen wide column ui left labeled icon input" style="padding-bottom: 5px;">
<label for="message" class="ui label">Message</label>
<input type="text" name="message" id="message" maxlength="40" placeholder="Message">
<i class="comment icon"></i>
</div>
<div class="right floated column">
<input type="submit" name="submit" class="ui button" id="send_msg" value="Send" />
<button type="button" class="ui button" id="send_beacon" value="Send GPS Beacon">Send GPS Beacon</button>
</div>
</form>
</div>
<div class="ui grid">
<div class="ui top attached tabular raised menu" id="callsignTabs">
</div>
<div class="sixteen wide column ui bottom attached raised tab segment" id="msgsTabsDiv" style="height:250px;padding:5px;">
&nbsp;
</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>

View File

@ -7,13 +7,12 @@
add-trailing-comma==2.3.0 # via gray add-trailing-comma==2.3.0 # via gray
alabaster==0.7.12 # via sphinx alabaster==0.7.12 # via sphinx
attrs==22.1.0 # via jsonschema, pytest attrs==22.1.0 # via jsonschema, pytest
autoflake==1.7.7 # via gray autoflake==2.0.0 # via gray
babel==2.11.0 # via sphinx babel==2.11.0 # via sphinx
black==22.10.0 # via gray black==22.10.0 # via gray
bleach==5.0.1 # via readme-renderer bleach==5.0.1 # via readme-renderer
build==0.9.0 # via pip-tools build==0.9.0 # via pip-tools
certifi==2022.9.24 # via requests certifi==2022.9.24 # via requests
cffi==1.15.1 # via cryptography
cfgv==3.3.1 # via pre-commit cfgv==3.3.1 # via pre-commit
charset-normalizer==2.1.1 # via requests charset-normalizer==2.1.1 # via requests
click==8.1.3 # via black, pip-tools click==8.1.3 # via black, pip-tools
@ -21,7 +20,6 @@ colorlog==6.7.0 # via prettylog
commonmark==0.9.1 # via rich commonmark==0.9.1 # via rich
configargparse==1.5.3 # via gray configargparse==1.5.3 # via gray
coverage[toml]==6.5.0 # via pytest-cov coverage[toml]==6.5.0 # via pytest-cov
cryptography==38.0.3 # via secretstorage
distlib==0.3.6 # via virtualenv distlib==0.3.6 # via virtualenv
docutils==0.19 # via readme-renderer, sphinx docutils==0.19 # via readme-renderer, sphinx
exceptiongroup==1.0.4 # via pytest exceptiongroup==1.0.4 # via pytest
@ -38,9 +36,8 @@ importlib-resources==5.10.0 # via fixit, jsonschema
iniconfig==1.1.1 # via pytest iniconfig==1.1.1 # via pytest
isort==5.10.1 # via -r dev-requirements.in, gray isort==5.10.1 # via -r dev-requirements.in, gray
jaraco-classes==3.2.3 # via keyring jaraco-classes==3.2.3 # via keyring
jeepney==0.8.0 # via keyring, secretstorage
jinja2==3.1.2 # via sphinx jinja2==3.1.2 # via sphinx
jsonschema==4.17.1 # via fixit jsonschema==4.17.3 # via fixit
keyring==23.11.0 # via twine keyring==23.11.0 # via twine
libcst==0.4.9 # via fixit libcst==0.4.9 # via fixit
markupsafe==2.1.1 # via jinja2 markupsafe==2.1.1 # via jinja2
@ -54,7 +51,7 @@ pathspec==0.10.2 # via black
pep517==0.13.0 # via build pep517==0.13.0 # via build
pep8-naming==0.13.2 # via -r dev-requirements.in pep8-naming==0.13.2 # via -r dev-requirements.in
pip-tools==6.10.0 # via -r dev-requirements.in pip-tools==6.10.0 # via -r dev-requirements.in
pkginfo==1.8.3 # via twine pkginfo==1.9.2 # via twine
pkgutil-resolve-name==1.3.10 # via jsonschema pkgutil-resolve-name==1.3.10 # via jsonschema
platformdirs==2.5.4 # via black, virtualenv platformdirs==2.5.4 # via black, virtualenv
pluggy==1.0.0 # via pytest, tox pluggy==1.0.0 # via pytest, tox
@ -62,7 +59,6 @@ pre-commit==2.20.0 # via -r dev-requirements.in
prettylog==0.3.0 # via gray prettylog==0.3.0 # via gray
py==1.11.0 # via tox py==1.11.0 # via tox
pycodestyle==2.10.0 # via flake8 pycodestyle==2.10.0 # via flake8
pycparser==2.21 # via cffi
pyflakes==3.0.1 # via autoflake, flake8 pyflakes==3.0.1 # via autoflake, flake8
pygments==2.13.0 # via readme-renderer, rich, sphinx pygments==2.13.0 # via readme-renderer, rich, sphinx
pyparsing==3.0.9 # via packaging pyparsing==3.0.9 # via packaging
@ -70,14 +66,13 @@ pyrsistent==0.19.2 # via jsonschema
pytest==7.2.0 # via -r dev-requirements.in, pytest-cov pytest==7.2.0 # via -r dev-requirements.in, pytest-cov
pytest-cov==4.0.0 # via -r dev-requirements.in pytest-cov==4.0.0 # via -r dev-requirements.in
pytz==2022.6 # via babel pytz==2022.6 # via babel
pyupgrade==3.2.2 # via gray pyupgrade==3.2.3 # via gray
pyyaml==6.0 # via fixit, libcst, pre-commit pyyaml==6.0 # via fixit, libcst, pre-commit
readme-renderer==37.3 # via twine readme-renderer==37.3 # via twine
requests==2.28.1 # via requests-toolbelt, sphinx, twine requests==2.28.1 # via requests-toolbelt, sphinx, twine
requests-toolbelt==0.10.1 # via twine requests-toolbelt==0.10.1 # via twine
rfc3986==2.0.0 # via twine rfc3986==2.0.0 # via twine
rich==12.6.0 # via twine rich==12.6.0 # via twine
secretstorage==3.3.3 # via keyring
six==1.16.0 # via bleach, tox six==1.16.0 # via bleach, tox
snowballstemmer==2.2.0 # via sphinx snowballstemmer==2.2.0 # via sphinx
sphinx==5.3.0 # via -r dev-requirements.in sphinx==5.3.0 # via -r dev-requirements.in
@ -98,10 +93,10 @@ ujson==5.5.0 # via fast-json
unify==0.5 # via gray unify==0.5 # via gray
untokenize==0.1.1 # via unify untokenize==0.1.1 # via unify
urllib3==1.26.13 # via requests, twine urllib3==1.26.13 # via requests, twine
virtualenv==20.16.7 # via pre-commit, tox virtualenv==20.17.0 # via pre-commit, tox
webencodings==0.5.1 # via bleach webencodings==0.5.1 # via bleach
wheel==0.38.4 # via pip-tools wheel==0.38.4 # via pip-tools
zipp==3.10.0 # via importlib-metadata, importlib-resources zipp==3.11.0 # via importlib-metadata, importlib-resources
# The following packages are considered to be unsafe in a requirements file: # The following packages are considered to be unsafe in a requirements file:
# pip # pip

View File

@ -24,3 +24,5 @@ wrapt
# kiss3 uses attrs # kiss3 uses attrs
kiss3 kiss3
attrs==22.1.0 attrs==22.1.0
# for mobile checking
device-detector

View File

@ -15,6 +15,7 @@ charset-normalizer==2.1.1 # via requests
click==8.1.3 # via -r requirements.in, click-completion, flask click==8.1.3 # via -r requirements.in, click-completion, flask
click-completion==0.5.2 # via -r requirements.in click-completion==0.5.2 # via -r requirements.in
commonmark==0.9.1 # via rich commonmark==0.9.1 # via rich
device-detector==5.0.1 # via -r requirements.in
dnspython==2.2.1 # via eventlet dnspython==2.2.1 # via eventlet
eventlet==0.33.2 # via -r requirements.in eventlet==0.33.2 # via -r requirements.in
flask==2.1.2 # via -r requirements.in, flask-classful, flask-httpauth, flask-socketio flask==2.1.2 # via -r requirements.in, flask-classful, flask-httpauth, flask-socketio
@ -37,7 +38,8 @@ pyserial-asyncio==0.6 # via kiss3
python-engineio==4.3.4 # via python-socketio python-engineio==4.3.4 # via python-socketio
python-socketio==5.7.2 # via flask-socketio python-socketio==5.7.2 # via flask-socketio
pytz==2022.6 # via -r requirements.in pytz==2022.6 # via -r requirements.in
pyyaml==6.0 # via -r requirements.in pyyaml==6.0 # via -r requirements.in, device-detector
regex==2022.10.31 # via device-detector
requests==2.28.1 # via -r requirements.in, update-checker requests==2.28.1 # via -r requirements.in, update-checker
rich==12.6.0 # via -r requirements.in rich==12.6.0 # via -r requirements.in
shellingham==1.5.0 # via click-completion shellingham==1.5.0 # via click-completion
@ -50,4 +52,4 @@ update-checker==0.18.0 # via -r requirements.in
urllib3==1.26.13 # via requests urllib3==1.26.13 # via requests
werkzeug==2.1.2 # via -r requirements.in, flask werkzeug==2.1.2 # via -r requirements.in, flask
wrapt==1.14.1 # via -r requirements.in wrapt==1.14.1 # via -r requirements.in
zipp==3.10.0 # via importlib-metadata zipp==3.11.0 # via importlib-metadata