2022-07-20 08:43:57 -04:00
|
|
|
|
import datetime
|
|
|
|
|
import json
|
|
|
|
|
import logging
|
2024-02-06 13:50:51 -05:00
|
|
|
|
import math
|
2022-07-20 08:43:57 -04:00
|
|
|
|
import signal
|
|
|
|
|
import sys
|
|
|
|
|
import threading
|
|
|
|
|
import time
|
|
|
|
|
|
|
|
|
|
import click
|
|
|
|
|
import flask
|
|
|
|
|
from flask import request
|
|
|
|
|
from flask_httpauth import HTTPBasicAuth
|
|
|
|
|
from flask_socketio import Namespace, SocketIO
|
2024-02-06 13:50:51 -05:00
|
|
|
|
from geopy.distance import geodesic
|
2022-12-27 14:30:03 -05:00
|
|
|
|
from oslo_config import cfg
|
2022-07-20 08:43:57 -04:00
|
|
|
|
from werkzeug.security import check_password_hash, generate_password_hash
|
|
|
|
|
import wrapt
|
|
|
|
|
|
|
|
|
|
import aprsd
|
2024-03-01 13:31:05 -05:00
|
|
|
|
from aprsd import (
|
|
|
|
|
cli_helper, client, packets, plugin_utils, stats, threads, utils,
|
|
|
|
|
)
|
2023-07-08 17:30:22 -04:00
|
|
|
|
from aprsd.main import cli
|
2024-02-06 13:50:51 -05:00
|
|
|
|
from aprsd.threads import aprsd as aprsd_threads
|
2024-04-05 12:42:50 -04:00
|
|
|
|
from aprsd.threads import keep_alive, rx, tx
|
2023-09-21 16:29:15 -04:00
|
|
|
|
from aprsd.utils import trace
|
2022-07-20 08:43:57 -04:00
|
|
|
|
|
|
|
|
|
|
2022-12-27 14:30:03 -05:00
|
|
|
|
CONF = cfg.CONF
|
2024-03-22 23:19:54 -04:00
|
|
|
|
LOG = logging.getLogger()
|
2022-07-20 08:43:57 -04:00
|
|
|
|
auth = HTTPBasicAuth()
|
2023-07-19 11:27:34 -04:00
|
|
|
|
users = {}
|
2022-12-27 14:30:03 -05:00
|
|
|
|
socketio = None
|
2022-07-20 08:43:57 -04:00
|
|
|
|
|
2024-02-06 13:50:51 -05:00
|
|
|
|
# List of callsigns that we don't want to track/fetch their location
|
|
|
|
|
callsign_no_track = [
|
|
|
|
|
"REPEAT", "WB4BOR-11", "APDW16", "WXNOW", "WXBOT", "BLN0", "BLN1", "BLN2",
|
|
|
|
|
"BLN3", "BLN4", "BLN5", "BLN6", "BLN7", "BLN8", "BLN9",
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
# Callsign location information
|
|
|
|
|
# callsign: {lat: 0.0, long: 0.0, last_update: datetime}
|
|
|
|
|
callsign_locations = {}
|
|
|
|
|
|
2023-07-19 11:27:34 -04:00
|
|
|
|
flask_app = flask.Flask(
|
|
|
|
|
"aprsd",
|
|
|
|
|
static_url_path="/static",
|
|
|
|
|
static_folder="web/chat/static",
|
|
|
|
|
template_folder="web/chat/templates",
|
|
|
|
|
)
|
|
|
|
|
|
2022-07-20 08:43:57 -04:00
|
|
|
|
|
2022-11-22 13:32:19 -05:00
|
|
|
|
def signal_handler(sig, frame):
|
|
|
|
|
|
|
|
|
|
click.echo("signal_handler: called")
|
|
|
|
|
LOG.info(
|
|
|
|
|
f"Ctrl+C, Sending all threads({len(threads.APRSDThreadList())}) exit! "
|
|
|
|
|
f"Can take up to 10 seconds {datetime.datetime.now()}",
|
|
|
|
|
)
|
|
|
|
|
threads.APRSDThreadList().stop_all()
|
|
|
|
|
if "subprocess" not in str(frame):
|
|
|
|
|
time.sleep(1.5)
|
|
|
|
|
# packets.WatchList().save()
|
|
|
|
|
# packets.SeenList().save()
|
2024-04-02 14:07:37 -04:00
|
|
|
|
LOG.info(stats.stats_collector.collect())
|
2022-11-22 13:32:19 -05:00
|
|
|
|
LOG.info("Telling flask to bail.")
|
|
|
|
|
signal.signal(signal.SIGTERM, sys.exit(0))
|
|
|
|
|
|
|
|
|
|
|
2023-09-21 16:29:15 -04:00
|
|
|
|
class SentMessages:
|
|
|
|
|
|
2022-07-20 08:43:57 -04:00
|
|
|
|
_instance = None
|
|
|
|
|
lock = threading.Lock()
|
|
|
|
|
|
|
|
|
|
data = {}
|
|
|
|
|
|
|
|
|
|
def __new__(cls, *args, **kwargs):
|
|
|
|
|
"""This magic turns this into a singleton."""
|
|
|
|
|
if cls._instance is None:
|
|
|
|
|
cls._instance = super().__new__(cls)
|
|
|
|
|
return cls._instance
|
|
|
|
|
|
2022-12-18 08:52:58 -05:00
|
|
|
|
def is_initialized(self):
|
|
|
|
|
return True
|
|
|
|
|
|
2022-07-20 08:43:57 -04:00
|
|
|
|
@wrapt.synchronized(lock)
|
|
|
|
|
def add(self, msg):
|
2023-09-21 16:29:15 -04:00
|
|
|
|
self.data[msg.msgNo] = msg.__dict__
|
2022-07-20 08:43:57 -04:00
|
|
|
|
|
|
|
|
|
@wrapt.synchronized(lock)
|
|
|
|
|
def __len__(self):
|
|
|
|
|
return len(self.data.keys())
|
|
|
|
|
|
|
|
|
|
@wrapt.synchronized(lock)
|
|
|
|
|
def get(self, id):
|
|
|
|
|
if id in self.data:
|
|
|
|
|
return self.data[id]
|
|
|
|
|
|
|
|
|
|
@wrapt.synchronized(lock)
|
|
|
|
|
def get_all(self):
|
|
|
|
|
return self.data
|
|
|
|
|
|
|
|
|
|
@wrapt.synchronized(lock)
|
|
|
|
|
def set_status(self, id, status):
|
2022-07-28 16:24:25 -04:00
|
|
|
|
if id in self.data:
|
|
|
|
|
self.data[id]["last_update"] = str(datetime.datetime.now())
|
|
|
|
|
self.data[id]["status"] = status
|
2022-07-20 08:43:57 -04:00
|
|
|
|
|
|
|
|
|
@wrapt.synchronized(lock)
|
|
|
|
|
def ack(self, id):
|
|
|
|
|
"""The message got an ack!"""
|
2022-07-28 16:24:25 -04:00
|
|
|
|
if id in self.data:
|
|
|
|
|
self.data[id]["last_update"] = str(datetime.datetime.now())
|
|
|
|
|
self.data[id]["ack"] = True
|
2022-07-20 08:43:57 -04:00
|
|
|
|
|
|
|
|
|
@wrapt.synchronized(lock)
|
|
|
|
|
def reply(self, id, packet):
|
|
|
|
|
"""We got a packet back from the sent message."""
|
2022-07-28 16:24:25 -04:00
|
|
|
|
if id in self.data:
|
|
|
|
|
self.data[id]["reply"] = packet
|
2022-07-20 08:43:57 -04:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# HTTPBasicAuth doesn't work on a class method.
|
|
|
|
|
# This has to be out here. Rely on the APRSDFlask
|
|
|
|
|
# class to initialize the users from the config
|
|
|
|
|
@auth.verify_password
|
|
|
|
|
def verify_password(username, password):
|
|
|
|
|
global users
|
|
|
|
|
|
2022-12-27 14:30:03 -05:00
|
|
|
|
if username in users and check_password_hash(users[username], password):
|
2022-07-20 08:43:57 -04:00
|
|
|
|
return username
|
|
|
|
|
|
|
|
|
|
|
2024-02-06 13:50:51 -05:00
|
|
|
|
def calculate_initial_compass_bearing(point_a, point_b):
|
|
|
|
|
"""
|
|
|
|
|
Calculates the bearing between two points.
|
|
|
|
|
The formulae used is the following:
|
|
|
|
|
θ = atan2(sin(Δlong).cos(lat2),
|
|
|
|
|
cos(lat1).sin(lat2) − sin(lat1).cos(lat2).cos(Δlong))
|
|
|
|
|
:Parameters:
|
|
|
|
|
- `pointA: The tuple representing the latitude/longitude for the
|
|
|
|
|
first point. Latitude and longitude must be in decimal degrees
|
|
|
|
|
- `pointB: The tuple representing the latitude/longitude for the
|
|
|
|
|
second point. Latitude and longitude must be in decimal degrees
|
|
|
|
|
:Returns:
|
|
|
|
|
The bearing in degrees
|
|
|
|
|
:Returns Type:
|
|
|
|
|
float
|
|
|
|
|
"""
|
|
|
|
|
if (type(point_a) is not tuple) or (type(point_b) is not tuple):
|
|
|
|
|
raise TypeError("Only tuples are supported as arguments")
|
|
|
|
|
|
|
|
|
|
lat1 = math.radians(point_a[0])
|
|
|
|
|
lat2 = math.radians(point_b[0])
|
|
|
|
|
|
|
|
|
|
diff_long = math.radians(point_b[1] - point_a[1])
|
|
|
|
|
|
|
|
|
|
x = math.sin(diff_long) * math.cos(lat2)
|
|
|
|
|
y = math.cos(lat1) * math.sin(lat2) - (
|
|
|
|
|
math.sin(lat1)
|
|
|
|
|
* math.cos(lat2) * math.cos(diff_long)
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
initial_bearing = math.atan2(x, y)
|
|
|
|
|
|
|
|
|
|
# Now we have the initial bearing but math.atan2 return values
|
|
|
|
|
# from -180° to + 180° which is not what we want for a compass bearing
|
|
|
|
|
# The solution is to normalize the initial bearing as shown below
|
|
|
|
|
initial_bearing = math.degrees(initial_bearing)
|
|
|
|
|
compass_bearing = (initial_bearing + 360) % 360
|
|
|
|
|
|
|
|
|
|
return compass_bearing
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _build_location_from_repeat(message):
|
|
|
|
|
# This is a location message Format is
|
|
|
|
|
# ^ld^callsign:latitude,longitude,altitude,course,speed,timestamp
|
|
|
|
|
a = message.split(":")
|
|
|
|
|
LOG.warning(a)
|
|
|
|
|
if len(a) == 2:
|
2024-03-01 13:31:05 -05:00
|
|
|
|
callsign = a[0].replace("^ld^", "")
|
2024-02-06 13:50:51 -05:00
|
|
|
|
b = a[1].split(",")
|
|
|
|
|
LOG.warning(b)
|
|
|
|
|
if len(b) == 6:
|
|
|
|
|
lat = float(b[0])
|
|
|
|
|
lon = float(b[1])
|
|
|
|
|
alt = float(b[2])
|
|
|
|
|
course = float(b[3])
|
|
|
|
|
speed = float(b[4])
|
|
|
|
|
time = int(b[5])
|
|
|
|
|
data = {
|
|
|
|
|
"callsign": callsign,
|
|
|
|
|
"lat": lat,
|
|
|
|
|
"lon": lon,
|
|
|
|
|
"altitude": alt,
|
|
|
|
|
"course": course,
|
|
|
|
|
"speed": speed,
|
|
|
|
|
"lasttime": time,
|
|
|
|
|
}
|
|
|
|
|
LOG.warning(f"Location data from REPEAT {data}")
|
|
|
|
|
return data
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _calculate_location_data(location_data):
|
|
|
|
|
"""Calculate all of the location data from data from aprs.fi or REPEAT."""
|
|
|
|
|
lat = location_data["lat"]
|
|
|
|
|
lon = location_data["lon"]
|
|
|
|
|
alt = location_data["altitude"]
|
|
|
|
|
speed = location_data["speed"]
|
|
|
|
|
lasttime = location_data["lasttime"]
|
|
|
|
|
# now calculate distance from our own location
|
|
|
|
|
distance = 0
|
|
|
|
|
if CONF.webchat.latitude and CONF.webchat.longitude:
|
|
|
|
|
our_lat = float(CONF.webchat.latitude)
|
|
|
|
|
our_lon = float(CONF.webchat.longitude)
|
|
|
|
|
distance = geodesic((our_lat, our_lon), (lat, lon)).kilometers
|
|
|
|
|
bearing = calculate_initial_compass_bearing(
|
|
|
|
|
(our_lat, our_lon),
|
|
|
|
|
(lat, lon),
|
|
|
|
|
)
|
|
|
|
|
return {
|
|
|
|
|
"callsign": location_data["callsign"],
|
|
|
|
|
"lat": lat,
|
|
|
|
|
"lon": lon,
|
|
|
|
|
"altitude": alt,
|
|
|
|
|
"course": f"{bearing:0.1f}",
|
|
|
|
|
"speed": speed,
|
|
|
|
|
"lasttime": lasttime,
|
|
|
|
|
"distance": f"{distance:0.3f}",
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def send_location_data_to_browser(location_data):
|
|
|
|
|
global socketio
|
|
|
|
|
callsign = location_data["callsign"]
|
|
|
|
|
LOG.info(f"Got location for {callsign} {callsign_locations[callsign]}")
|
|
|
|
|
socketio.emit(
|
|
|
|
|
"callsign_location", callsign_locations[callsign],
|
|
|
|
|
namespace="/sendmsg",
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def populate_callsign_location(callsign, data=None):
|
|
|
|
|
"""Populate the location for the callsign.
|
|
|
|
|
|
|
|
|
|
if data is passed in, then we have the location already from
|
|
|
|
|
an APRS packet. If data is None, then we need to fetch the
|
|
|
|
|
location from aprs.fi or REPEAT.
|
|
|
|
|
"""
|
|
|
|
|
global socketio
|
|
|
|
|
"""Fetch the location for the callsign."""
|
|
|
|
|
LOG.debug(f"populate_callsign_location {callsign}")
|
|
|
|
|
if data:
|
|
|
|
|
location_data = _calculate_location_data(data)
|
|
|
|
|
callsign_locations[callsign] = location_data
|
|
|
|
|
send_location_data_to_browser(location_data)
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
# First we are going to try to get the location from aprs.fi
|
|
|
|
|
# if there is no internets, then this will fail and we will
|
|
|
|
|
# fallback to calling REPEAT for the location for the callsign.
|
|
|
|
|
fallback = False
|
|
|
|
|
if not CONF.aprs_fi.apiKey:
|
|
|
|
|
LOG.warning(
|
|
|
|
|
"Config aprs_fi.apiKey is not set. Can't get location from aprs.fi "
|
|
|
|
|
" falling back to sending REPEAT to get location.",
|
|
|
|
|
)
|
|
|
|
|
fallback = True
|
|
|
|
|
else:
|
|
|
|
|
try:
|
|
|
|
|
aprs_data = plugin_utils.get_aprs_fi(CONF.aprs_fi.apiKey, callsign)
|
|
|
|
|
if not len(aprs_data["entries"]):
|
|
|
|
|
LOG.error("Didn't get any entries from aprs.fi")
|
|
|
|
|
return
|
|
|
|
|
lat = float(aprs_data["entries"][0]["lat"])
|
|
|
|
|
lon = float(aprs_data["entries"][0]["lng"])
|
|
|
|
|
try: # altitude not always provided
|
|
|
|
|
alt = float(aprs_data["entries"][0]["altitude"])
|
|
|
|
|
except Exception:
|
|
|
|
|
alt = 0
|
|
|
|
|
location_data = {
|
2024-03-01 13:31:05 -05:00
|
|
|
|
"callsign": callsign,
|
|
|
|
|
"lat": lat,
|
|
|
|
|
"lon": lon,
|
|
|
|
|
"altitude": alt,
|
|
|
|
|
"lasttime": int(aprs_data["entries"][0]["lasttime"]),
|
|
|
|
|
"course": float(aprs_data["entries"][0].get("course", 0)),
|
|
|
|
|
"speed": float(aprs_data["entries"][0].get("speed", 0)),
|
2024-02-06 13:50:51 -05:00
|
|
|
|
}
|
|
|
|
|
location_data = _calculate_location_data(location_data)
|
|
|
|
|
callsign_locations[callsign] = location_data
|
|
|
|
|
send_location_data_to_browser(location_data)
|
|
|
|
|
return
|
|
|
|
|
except Exception as ex:
|
|
|
|
|
LOG.error(f"Failed to fetch aprs.fi '{ex}'")
|
2024-02-19 20:15:56 -05:00
|
|
|
|
LOG.error(ex)
|
2024-02-06 13:50:51 -05:00
|
|
|
|
fallback = True
|
|
|
|
|
|
|
|
|
|
if fallback:
|
|
|
|
|
# We don't have the location data
|
|
|
|
|
# and we can't get it from aprs.fi
|
|
|
|
|
# Send a special message to REPEAT to get the location data
|
|
|
|
|
LOG.info(f"Sending REPEAT to get location for callsign {callsign}.")
|
|
|
|
|
tx.send(
|
|
|
|
|
packets.MessagePacket(
|
|
|
|
|
from_call=CONF.callsign,
|
|
|
|
|
to_call="REPEAT",
|
|
|
|
|
message_text=f"ld {callsign}",
|
|
|
|
|
),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
2022-12-02 16:26:48 -05:00
|
|
|
|
class WebChatProcessPacketThread(rx.APRSDProcessPacketThread):
|
|
|
|
|
"""Class that handles packets being sent to us."""
|
2024-02-06 13:50:51 -05:00
|
|
|
|
|
2022-12-27 14:30:03 -05:00
|
|
|
|
def __init__(self, packet_queue, socketio):
|
2022-07-20 08:43:57 -04:00
|
|
|
|
self.socketio = socketio
|
|
|
|
|
self.connected = False
|
2022-12-27 14:30:03 -05:00
|
|
|
|
super().__init__(packet_queue)
|
2022-07-20 08:43:57 -04:00
|
|
|
|
|
2022-12-14 22:03:21 -05:00
|
|
|
|
def process_ack_packet(self, packet: packets.AckPacket):
|
2022-12-02 16:26:48 -05:00
|
|
|
|
super().process_ack_packet(packet)
|
2022-07-20 08:43:57 -04:00
|
|
|
|
ack_num = packet.get("msgNo")
|
2023-09-29 15:40:42 -04:00
|
|
|
|
SentMessages().ack(ack_num)
|
2024-02-06 13:50:51 -05:00
|
|
|
|
msg = SentMessages().get(ack_num)
|
|
|
|
|
if msg:
|
|
|
|
|
self.socketio.emit(
|
|
|
|
|
"ack", msg,
|
|
|
|
|
namespace="/sendmsg",
|
|
|
|
|
)
|
2022-07-20 08:43:57 -04:00
|
|
|
|
self.got_ack = True
|
|
|
|
|
|
2022-12-14 22:03:21 -05:00
|
|
|
|
def process_our_message_packet(self, packet: packets.MessagePacket):
|
2024-02-06 13:50:51 -05:00
|
|
|
|
global callsign_locations
|
|
|
|
|
# ok lets see if we have the location for the
|
|
|
|
|
# person we just sent a message to.
|
|
|
|
|
from_call = packet.get("from_call").upper()
|
2024-03-01 13:31:05 -05:00
|
|
|
|
if from_call == "REPEAT":
|
2024-02-06 13:50:51 -05:00
|
|
|
|
# We got a message from REPEAT. Is this a location message?
|
|
|
|
|
message = packet.get("message_text")
|
|
|
|
|
if message.startswith("^ld^"):
|
|
|
|
|
location_data = _build_location_from_repeat(message)
|
|
|
|
|
callsign = location_data["callsign"]
|
|
|
|
|
location_data = _calculate_location_data(location_data)
|
|
|
|
|
callsign_locations[callsign] = location_data
|
|
|
|
|
send_location_data_to_browser(location_data)
|
|
|
|
|
return
|
2024-02-19 20:15:56 -05:00
|
|
|
|
elif (
|
|
|
|
|
from_call not in callsign_locations
|
|
|
|
|
and from_call not in callsign_no_track
|
|
|
|
|
):
|
2024-02-06 13:50:51 -05:00
|
|
|
|
# We have to ask aprs for the location for the callsign
|
|
|
|
|
# We send a message packet to wb4bor-11 asking for location.
|
|
|
|
|
populate_callsign_location(from_call)
|
|
|
|
|
# Send the packet to the browser.
|
2022-12-02 16:26:48 -05:00
|
|
|
|
self.socketio.emit(
|
2023-09-21 16:29:15 -04:00
|
|
|
|
"new", packet.__dict__,
|
2022-12-02 16:26:48 -05:00
|
|
|
|
namespace="/sendmsg",
|
|
|
|
|
)
|
2022-07-20 08:43:57 -04:00
|
|
|
|
|
|
|
|
|
|
2024-02-06 13:50:51 -05:00
|
|
|
|
class LocationProcessingThread(aprsd_threads.APRSDThread):
|
|
|
|
|
"""Class to handle the location processing."""
|
|
|
|
|
def __init__(self):
|
|
|
|
|
super().__init__("LocationProcessingThread")
|
|
|
|
|
|
|
|
|
|
def loop(self):
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
2023-07-19 11:27:34 -04:00
|
|
|
|
def set_config():
|
|
|
|
|
global users
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _get_transport(stats):
|
|
|
|
|
if CONF.aprs_network.enabled:
|
|
|
|
|
transport = "aprs-is"
|
|
|
|
|
aprs_connection = (
|
|
|
|
|
"APRS-IS Server: <a href='http://status.aprs2.net' >"
|
2024-04-02 14:07:37 -04:00
|
|
|
|
"{}</a>".format(stats["APRSClientStats"]["server_string"])
|
2023-07-19 11:27:34 -04:00
|
|
|
|
)
|
2023-10-05 10:33:07 -04:00
|
|
|
|
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,
|
2023-07-19 11:27:34 -04:00
|
|
|
|
)
|
2023-10-05 10:33:07 -04:00
|
|
|
|
)
|
|
|
|
|
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"
|
2023-07-19 11:27:34 -04:00
|
|
|
|
|
|
|
|
|
return transport, aprs_connection
|
|
|
|
|
|
|
|
|
|
|
2024-02-19 20:15:56 -05:00
|
|
|
|
@flask_app.route("/location/<callsign>", methods=["POST"])
|
|
|
|
|
def location(callsign):
|
|
|
|
|
LOG.debug(f"Fetch location for callsign {callsign}")
|
|
|
|
|
populate_callsign_location(callsign)
|
|
|
|
|
|
|
|
|
|
|
2023-07-19 11:27:34 -04:00
|
|
|
|
@auth.login_required
|
|
|
|
|
@flask_app.route("/")
|
|
|
|
|
def index():
|
|
|
|
|
stats = _stats()
|
|
|
|
|
|
|
|
|
|
# For development
|
2023-09-06 11:20:59 -04:00
|
|
|
|
html_template = "index.html"
|
2023-07-19 11:27:34 -04:00
|
|
|
|
LOG.debug(f"Template {html_template}")
|
2022-11-30 14:07:16 -05:00
|
|
|
|
|
2024-04-02 14:07:37 -04:00
|
|
|
|
transport, aprs_connection = _get_transport(stats["stats"])
|
2023-07-19 11:27:34 -04:00
|
|
|
|
LOG.debug(f"transport {transport} aprs_connection {aprs_connection}")
|
2022-11-30 14:07:16 -05:00
|
|
|
|
|
2023-07-19 11:27:34 -04:00
|
|
|
|
stats["transport"] = transport
|
|
|
|
|
stats["aprs_connection"] = aprs_connection
|
|
|
|
|
LOG.debug(f"initial stats = {stats}")
|
2023-08-22 12:31:44 -04:00
|
|
|
|
latitude = CONF.webchat.latitude
|
|
|
|
|
if latitude:
|
|
|
|
|
latitude = float(CONF.webchat.latitude)
|
|
|
|
|
|
|
|
|
|
longitude = CONF.webchat.longitude
|
|
|
|
|
if longitude:
|
|
|
|
|
longitude = float(longitude)
|
2022-11-30 14:07:16 -05:00
|
|
|
|
|
2023-07-19 11:27:34 -04:00
|
|
|
|
return flask.render_template(
|
|
|
|
|
html_template,
|
|
|
|
|
initial_stats=stats,
|
|
|
|
|
aprs_connection=aprs_connection,
|
|
|
|
|
callsign=CONF.callsign,
|
|
|
|
|
version=aprsd.__version__,
|
2023-08-22 12:31:44 -04:00
|
|
|
|
latitude=latitude,
|
|
|
|
|
longitude=longitude,
|
2023-07-19 11:27:34 -04:00
|
|
|
|
)
|
2022-11-30 14:07:16 -05:00
|
|
|
|
|
|
|
|
|
|
2023-07-19 11:27:34 -04:00
|
|
|
|
@auth.login_required
|
2024-02-19 20:15:56 -05:00
|
|
|
|
@flask_app.route("/send-message-status")
|
2023-07-19 11:27:34 -04:00
|
|
|
|
def send_message_status():
|
|
|
|
|
LOG.debug(request)
|
|
|
|
|
msgs = SentMessages()
|
|
|
|
|
info = msgs.get_all()
|
|
|
|
|
return json.dumps(info)
|
2022-07-20 08:43:57 -04:00
|
|
|
|
|
|
|
|
|
|
2023-07-19 11:27:34 -04:00
|
|
|
|
def _stats():
|
|
|
|
|
now = datetime.datetime.now()
|
2022-07-20 08:43:57 -04:00
|
|
|
|
|
2023-07-19 11:27:34 -04:00
|
|
|
|
time_format = "%m-%d-%Y %H:%M:%S"
|
2024-04-02 14:07:37 -04:00
|
|
|
|
stats_dict = stats.stats_collector.collect(serializable=True)
|
2023-07-19 11:27:34 -04:00
|
|
|
|
# Webchat doesnt need these
|
2024-04-02 14:07:37 -04:00
|
|
|
|
if "WatchList" in stats_dict:
|
|
|
|
|
del stats_dict["WatchList"]
|
2024-04-05 15:24:11 -04:00
|
|
|
|
if "SeenList" in stats_dict:
|
|
|
|
|
del stats_dict["SeenList"]
|
2024-04-02 14:07:37 -04:00
|
|
|
|
if "APRSDThreadList" in stats_dict:
|
|
|
|
|
del stats_dict["APRSDThreadList"]
|
2024-04-05 15:24:11 -04:00
|
|
|
|
if "PacketList" in stats_dict:
|
|
|
|
|
del stats_dict["PacketList"]
|
|
|
|
|
if "EmailStats" in stats_dict:
|
|
|
|
|
del stats_dict["EmailStats"]
|
|
|
|
|
if "PluginManager" in stats_dict:
|
|
|
|
|
del stats_dict["PluginManager"]
|
2022-07-20 08:43:57 -04:00
|
|
|
|
|
2023-07-19 11:27:34 -04:00
|
|
|
|
result = {
|
|
|
|
|
"time": now.strftime(time_format),
|
|
|
|
|
"stats": stats_dict,
|
|
|
|
|
}
|
|
|
|
|
return result
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@flask_app.route("/stats")
|
|
|
|
|
def get_stats():
|
|
|
|
|
return json.dumps(_stats())
|
2022-07-20 08:43:57 -04:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class SendMessageNamespace(Namespace):
|
|
|
|
|
"""Class to handle the socketio interactions."""
|
|
|
|
|
got_ack = False
|
|
|
|
|
reply_sent = False
|
|
|
|
|
msg = None
|
|
|
|
|
request = None
|
|
|
|
|
|
2022-12-02 16:26:48 -05:00
|
|
|
|
def __init__(self, namespace=None, config=None):
|
2022-07-20 08:43:57 -04:00
|
|
|
|
super().__init__(namespace)
|
|
|
|
|
|
|
|
|
|
def on_connect(self):
|
|
|
|
|
global socketio
|
|
|
|
|
LOG.debug("Web socket connected")
|
|
|
|
|
socketio.emit(
|
|
|
|
|
"connected", {"data": "/sendmsg Connected"},
|
|
|
|
|
namespace="/sendmsg",
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def on_disconnect(self):
|
|
|
|
|
LOG.debug("WS Disconnected")
|
|
|
|
|
|
|
|
|
|
def on_send(self, data):
|
|
|
|
|
global socketio
|
|
|
|
|
LOG.debug(f"WS: on_send {data}")
|
|
|
|
|
self.request = data
|
2022-12-27 14:30:03 -05:00
|
|
|
|
data["from"] = CONF.callsign
|
2023-10-05 10:33:07 -04:00
|
|
|
|
path = data.get("path", None)
|
2023-10-05 13:56:02 -04:00
|
|
|
|
if not path:
|
|
|
|
|
path = []
|
|
|
|
|
elif "," in path:
|
2023-10-05 10:33:07 -04:00
|
|
|
|
path_opts = path.split(",")
|
|
|
|
|
path = [x.strip() for x in path_opts]
|
|
|
|
|
else:
|
|
|
|
|
path = [path]
|
|
|
|
|
|
2022-12-15 17:23:54 -05:00
|
|
|
|
pkt = packets.MessagePacket(
|
|
|
|
|
from_call=data["from"],
|
|
|
|
|
to_call=data["to"].upper(),
|
|
|
|
|
message_text=data["message"],
|
2023-10-05 10:33:07 -04:00
|
|
|
|
path=path,
|
2022-07-20 08:43:57 -04:00
|
|
|
|
)
|
2023-09-21 16:29:15 -04:00
|
|
|
|
pkt.prepare()
|
2022-12-15 17:23:54 -05:00
|
|
|
|
self.msg = pkt
|
2022-07-20 08:43:57 -04:00
|
|
|
|
msgs = SentMessages()
|
2022-12-15 17:23:54 -05:00
|
|
|
|
msgs.add(pkt)
|
2022-12-21 16:26:36 -05:00
|
|
|
|
tx.send(pkt)
|
2022-12-15 17:23:54 -05:00
|
|
|
|
msgs.set_status(pkt.msgNo, "Sending")
|
|
|
|
|
obj = msgs.get(pkt.msgNo)
|
2022-07-20 08:43:57 -04:00
|
|
|
|
socketio.emit(
|
2022-11-22 13:32:19 -05:00
|
|
|
|
"sent", obj,
|
2022-07-20 08:43:57 -04:00
|
|
|
|
namespace="/sendmsg",
|
|
|
|
|
)
|
|
|
|
|
|
2022-11-24 11:27:58 -05:00
|
|
|
|
def on_gps(self, data):
|
|
|
|
|
LOG.debug(f"WS on_GPS: {data}")
|
2024-03-23 16:59:33 -04:00
|
|
|
|
lat = data["latitude"]
|
|
|
|
|
long = data["longitude"]
|
|
|
|
|
LOG.debug(f"Lat {lat}")
|
|
|
|
|
LOG.debug(f"Long {long}")
|
2024-04-17 12:34:01 -04:00
|
|
|
|
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]
|
2022-11-24 11:27:58 -05:00
|
|
|
|
|
2022-12-21 16:26:36 -05:00
|
|
|
|
tx.send(
|
2024-04-02 14:07:37 -04:00
|
|
|
|
packets.BeaconPacket(
|
2022-12-27 14:30:03 -05:00
|
|
|
|
from_call=CONF.callsign,
|
2022-12-21 16:26:36 -05:00
|
|
|
|
to_call="APDW16",
|
|
|
|
|
latitude=lat,
|
|
|
|
|
longitude=long,
|
|
|
|
|
comment="APRSD WebChat Beacon",
|
2024-04-17 12:34:01 -04:00
|
|
|
|
path=path,
|
2022-12-21 16:26:36 -05:00
|
|
|
|
),
|
|
|
|
|
direct=True,
|
2022-12-15 17:23:54 -05:00
|
|
|
|
)
|
2022-11-24 11:27:58 -05:00
|
|
|
|
|
2022-07-20 08:43:57 -04:00
|
|
|
|
def handle_message(self, data):
|
|
|
|
|
LOG.debug(f"WS Data {data}")
|
|
|
|
|
|
|
|
|
|
def handle_json(self, data):
|
|
|
|
|
LOG.debug(f"WS json {data}")
|
|
|
|
|
|
2024-02-19 20:15:56 -05:00
|
|
|
|
def on_get_callsign_location(self, data):
|
|
|
|
|
LOG.debug(f"on_callsign_location {data}")
|
|
|
|
|
populate_callsign_location(data["callsign"])
|
|
|
|
|
|
2022-07-20 08:43:57 -04:00
|
|
|
|
|
|
|
|
|
@trace.trace
|
2022-12-27 14:30:03 -05:00
|
|
|
|
def init_flask(loglevel, quiet):
|
2023-07-19 11:27:34 -04:00
|
|
|
|
global socketio, flask_app
|
2022-07-20 08:43:57 -04:00
|
|
|
|
|
|
|
|
|
socketio = SocketIO(
|
|
|
|
|
flask_app, logger=False, engineio_logger=False,
|
|
|
|
|
async_mode="threading",
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
socketio.on_namespace(
|
|
|
|
|
SendMessageNamespace(
|
2022-12-27 14:30:03 -05:00
|
|
|
|
"/sendmsg",
|
2022-07-20 08:43:57 -04:00
|
|
|
|
),
|
|
|
|
|
)
|
2023-07-19 11:27:34 -04:00
|
|
|
|
return socketio
|
2022-07-20 08:43:57 -04:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# main() ###
|
|
|
|
|
@cli.command()
|
|
|
|
|
@cli_helper.add_options(cli_helper.common_options)
|
|
|
|
|
@click.option(
|
|
|
|
|
"-f",
|
|
|
|
|
"--flush",
|
|
|
|
|
"flush",
|
|
|
|
|
is_flag=True,
|
|
|
|
|
show_default=True,
|
|
|
|
|
default=False,
|
|
|
|
|
help="Flush out all old aged messages on disk.",
|
|
|
|
|
)
|
|
|
|
|
@click.option(
|
|
|
|
|
"-p",
|
|
|
|
|
"--port",
|
|
|
|
|
"port",
|
|
|
|
|
show_default=True,
|
2023-08-22 11:58:26 -04:00
|
|
|
|
default=None,
|
|
|
|
|
help="Port to listen to web requests. This overrides the config.webchat.web_port setting.",
|
2022-07-20 08:43:57 -04:00
|
|
|
|
)
|
|
|
|
|
@click.pass_context
|
|
|
|
|
@cli_helper.process_standard_options
|
|
|
|
|
def webchat(ctx, flush, port):
|
|
|
|
|
"""Web based HAM Radio chat program!"""
|
|
|
|
|
loglevel = ctx.obj["loglevel"]
|
|
|
|
|
quiet = ctx.obj["quiet"]
|
|
|
|
|
|
2022-11-22 13:32:19 -05:00
|
|
|
|
signal.signal(signal.SIGINT, signal_handler)
|
|
|
|
|
signal.signal(signal.SIGTERM, signal_handler)
|
2022-07-20 08:43:57 -04:00
|
|
|
|
|
|
|
|
|
level, msg = utils._check_version()
|
|
|
|
|
if level:
|
|
|
|
|
LOG.warning(msg)
|
|
|
|
|
else:
|
|
|
|
|
LOG.info(msg)
|
|
|
|
|
LOG.info(f"APRSD Started version: {aprsd.__version__}")
|
|
|
|
|
|
2024-03-22 23:19:54 -04:00
|
|
|
|
CONF.log_opt_values(logging.getLogger(), logging.DEBUG)
|
2023-07-19 11:27:34 -04:00
|
|
|
|
user = CONF.admin.user
|
|
|
|
|
users[user] = generate_password_hash(CONF.admin.password)
|
2023-08-22 11:58:26 -04:00
|
|
|
|
if not port:
|
|
|
|
|
port = CONF.webchat.web_port
|
2022-07-20 08:43:57 -04:00
|
|
|
|
|
|
|
|
|
# Initialize the client factory and create
|
|
|
|
|
# The correct client object ready for use
|
2022-12-27 14:30:03 -05:00
|
|
|
|
client.ClientFactory.setup()
|
2022-07-20 08:43:57 -04:00
|
|
|
|
# Make sure we have 1 client transport enabled
|
|
|
|
|
if not client.factory.is_client_enabled():
|
|
|
|
|
LOG.error("No Clients are enabled in config.")
|
|
|
|
|
sys.exit(-1)
|
|
|
|
|
|
|
|
|
|
if not client.factory.is_client_configured():
|
|
|
|
|
LOG.error("APRS client is not properly configured in config file.")
|
|
|
|
|
sys.exit(-1)
|
|
|
|
|
|
2022-12-27 14:30:03 -05:00
|
|
|
|
packets.PacketList()
|
|
|
|
|
packets.PacketTrack()
|
|
|
|
|
packets.WatchList()
|
|
|
|
|
packets.SeenList()
|
2022-07-20 08:43:57 -04:00
|
|
|
|
|
2024-04-05 12:42:50 -04:00
|
|
|
|
keepalive = keep_alive.KeepAliveThread()
|
2023-08-23 13:45:46 -04:00
|
|
|
|
LOG.info("Start KeepAliveThread")
|
|
|
|
|
keepalive.start()
|
|
|
|
|
|
2023-07-19 11:27:34 -04:00
|
|
|
|
socketio = init_flask(loglevel, quiet)
|
2022-12-19 10:28:22 -05:00
|
|
|
|
rx_thread = rx.APRSDPluginRXThread(
|
|
|
|
|
packet_queue=threads.packet_queue,
|
2022-07-20 08:43:57 -04:00
|
|
|
|
)
|
|
|
|
|
rx_thread.start()
|
2022-12-19 10:28:22 -05:00
|
|
|
|
process_thread = WebChatProcessPacketThread(
|
|
|
|
|
packet_queue=threads.packet_queue,
|
|
|
|
|
socketio=socketio,
|
|
|
|
|
)
|
|
|
|
|
process_thread.start()
|
2022-07-20 08:43:57 -04:00
|
|
|
|
|
2022-11-22 13:32:19 -05:00
|
|
|
|
LOG.info("Start socketio.run()")
|
2022-07-20 08:43:57 -04:00
|
|
|
|
socketio.run(
|
2023-07-19 11:27:34 -04:00
|
|
|
|
flask_app,
|
2023-08-14 18:32:25 -04:00
|
|
|
|
# This is broken for now after removing cryptography
|
|
|
|
|
# and pyopenssl
|
|
|
|
|
# ssl_context="adhoc",
|
2023-08-22 11:58:26 -04:00
|
|
|
|
host=CONF.webchat.web_ip,
|
2022-07-20 08:43:57 -04:00
|
|
|
|
port=port,
|
2023-08-15 17:42:56 -04:00
|
|
|
|
allow_unsafe_werkzeug=True,
|
2022-07-20 08:43:57 -04:00
|
|
|
|
)
|
2022-11-22 13:32:19 -05:00
|
|
|
|
|
|
|
|
|
LOG.info("WebChat exiting!!!! Bye.")
|