Various stuff

This commit is contained in:
Ian Renton
2025-09-30 20:51:56 +01:00
parent 280749919d
commit 37692f41a8
9 changed files with 100 additions and 56 deletions

View File

@@ -7,28 +7,28 @@ SOFTWARE_VERSION = "0.1"
# Modes # Modes
CW_MODES = ["CW"] CW_MODES = ["CW"]
PHONE_MODES = ["PHONE", "SSB", "USB", "LSB", "AM", "FM", "DV", "DMR", "DSTAR", "C4FM", "M17"] PHONE_MODES = ["PHONE", "SSB", "USB", "LSB", "AM", "FM", "DV", "DMR", "DSTAR", "C4FM", "M17"]
DATA_MODES = ["DIGI", "DATA", "FT8", "FT4", "RTTY", "SSTV", "JS8", "HELL", "BPSK", "PSK", "BPSK31", "OLIVIA"] DATA_MODES = ["DIGI", "DATA", "DIGITAL", "FT8", "FT4", "RTTY", "SSTV", "JS8", "HELL", "BPSK", "PSK", "BPSK31", "OLIVIA"]
ALL_MODES = CW_MODES + PHONE_MODES + DATA_MODES ALL_MODES = CW_MODES + PHONE_MODES + DATA_MODES
# Band definitions # Band definitions
BANDS = [ BANDS = [
Band(name="160m", start_freq=1800, end_freq=2000), Band(name="160m", start_freq=1800, end_freq=2000, color="#7cfc00", contrast_color="black"),
Band(name="80m", start_freq=3500, end_freq=4000), Band(name="80m", start_freq=3500, end_freq=4000, color="#e550e5", contrast_color="black"),
Band(name="60m", start_freq=5250, end_freq=5410), Band(name="60m", start_freq=5250, end_freq=5410, color="#00008b", contrast_color="white"),
Band(name="40m", start_freq=7000, end_freq=7300), Band(name="40m", start_freq=7000, end_freq=7300, color="#5959ff", contrast_color="white"),
Band(name="30m", start_freq=10100, end_freq=10150), Band(name="30m", start_freq=10100, end_freq=10150, color="#62d962", contrast_color="black"),
Band(name="20m", start_freq=14000, end_freq=14350), Band(name="20m", start_freq=14000, end_freq=14350, color="#f2c40c", contrast_color="black"),
Band(name="17m", start_freq=18068, end_freq=18168), Band(name="17m", start_freq=18068, end_freq=18168, color="#f2f261", contrast_color="black"),
Band(name="15m", start_freq=21000, end_freq=21450), Band(name="15m", start_freq=21000, end_freq=21450, color="#cca166", contrast_color="black"),
Band(name="12m", start_freq=24890, end_freq=24990), Band(name="12m", start_freq=24890, end_freq=24990, color="#b22222", contrast_color="white"),
Band(name="10m", start_freq=28000, end_freq=29700), Band(name="10m", start_freq=28000, end_freq=29700, color="#ff69b4", contrast_color="black"),
Band(name="6m", start_freq=50000, end_freq=54000), Band(name="6m", start_freq=50000, end_freq=54000, color="#FF0000", contrast_color="white"),
Band(name="4m", start_freq=70000, end_freq=70500), Band(name="4m", start_freq=70000, end_freq=70500, color="#cc0044", contrast_color="white"),
Band(name="2m", start_freq=144000, end_freq=148000), Band(name="2m", start_freq=144000, end_freq=148000, color="#FF1493", contrast_color="black"),
Band(name="70cm", start_freq=420000, end_freq=450000), Band(name="70cm", start_freq=420000, end_freq=450000, color="#999900", contrast_color="white"),
Band(name="23cm", start_freq=1240000, end_freq=1325000), Band(name="23cm", start_freq=1240000, end_freq=1325000, color="#5AB8C7", contrast_color="black"),
Band(name="13cm", start_freq=2300000, end_freq=2450000)] Band(name="13cm", start_freq=2300000, end_freq=2450000, color="#FF7F50", contrast_color="black")]
UNKNOWN_BAND = Band(name="Unknown", start_freq=0, end_freq=0) UNKNOWN_BAND = Band(name="Unknown", start_freq=0, end_freq=0, color="black", contrast_color="white")
# DXCC flags (borrowed from https:#github.com/wavelog/wavelog/blob/master/application/libraries/DxccFlag.php) # DXCC flags (borrowed from https:#github.com/wavelog/wavelog/blob/master/application/libraries/DxccFlag.php)
DXCC_FLAGS = { DXCC_FLAGS = {

View File

@@ -3,6 +3,7 @@ from datetime import datetime
from diskcache import Cache from diskcache import Cache
from pyhamtools import LookupLib, Callinfo from pyhamtools import LookupLib, Callinfo
from pyhamtools.frequency import freq_to_band
from pyhamtools.locator import latlong_to_locator from pyhamtools.locator import latlong_to_locator
from core.config import config from core.config import config
@@ -25,7 +26,7 @@ def infer_mode_from_comment(comment):
return None return None
# Infer a "mode family" from a mode. # Infer a "mode family" from a mode.
def infer_mode_family_from_mode(mode): def infer_mode_type_from_mode(mode):
if mode.upper() in CW_MODES: if mode.upper() in CW_MODES:
return "CW" return "CW"
elif mode.upper() in PHONE_MODES: elif mode.upper() in PHONE_MODES:
@@ -137,6 +138,11 @@ def infer_grid_from_callsign_dxcc(call):
return latlong_to_locator(latlon[0], latlon[1], 8) return latlong_to_locator(latlon[0], latlon[1], 8)
# Infer a mode from the frequency according to the band plan. Just a guess really.
def infer_mode_from_frequency(freq):
return freq_to_band(freq)["mode"]
# Convert objects to serialisable things. Used by JSON serialiser as a default when it encounters unserializable things. # Convert objects to serialisable things. Used by JSON serialiser as a default when it encounters unserializable things.
# Converts datetimes to ISO. # Converts datetimes to ISO.
# Anything else it tries to convert to a dict. # Anything else it tries to convert to a dict.

View File

@@ -9,3 +9,7 @@ class Band:
start_freq: float start_freq: float
# Stop frequency, in kHz # Stop frequency, in kHz
end_freq: float end_freq: float
# Colour to use for this band, as per PSK Reporter
color: str
# Contrast colour to use for text against a background of the band colour
contrast_color: str

View File

@@ -7,10 +7,10 @@ import pytz
from pyhamtools.locator import locator_to_latlong, latlong_to_locator from pyhamtools.locator import locator_to_latlong, latlong_to_locator
from core.constants import DXCC_FLAGS from core.constants import DXCC_FLAGS
from core.utils import infer_mode_family_from_mode, infer_band_from_freq, infer_continent_from_callsign, \ from core.utils import infer_mode_type_from_mode, infer_band_from_freq, infer_continent_from_callsign, \
infer_country_from_callsign, infer_cq_zone_from_callsign, infer_itu_zone_from_callsign, infer_dxcc_id_from_callsign, \ infer_country_from_callsign, infer_cq_zone_from_callsign, infer_itu_zone_from_callsign, infer_dxcc_id_from_callsign, \
infer_mode_from_comment, infer_name_from_callsign, infer_latlon_from_callsign_dxcc, infer_grid_from_callsign_dxcc, \ infer_mode_from_comment, infer_name_from_callsign, infer_latlon_from_callsign_dxcc, infer_grid_from_callsign_dxcc, \
infer_latlon_from_callsign_qrz, infer_grid_from_callsign_qrz infer_latlon_from_callsign_qrz, infer_grid_from_callsign_qrz, infer_mode_from_frequency
# Data class that defines a spot. # Data class that defines a spot.
@@ -50,7 +50,9 @@ class Spot:
# Reported mode, such as SSB, PHONE, CW, FT8... # Reported mode, such as SSB, PHONE, CW, FT8...
mode: str = None mode: str = None
# Inferred mode "family". One of "CW", "PHONE" or "DIGI". # Inferred mode "family". One of "CW", "PHONE" or "DIGI".
mode_family: str = None mode_type: str = None
# Source of the mode information. "SPOT", "COMMENT", "BANDPLAN" or "NONE"
mode_source: str = "NONE"
# Frequency, in kHz # Frequency, in kHz
freq: float = None freq: float = None
# Band, defined by the frequency, e.g. "40m" or "70cm" # Band, defined by the frequency, e.g. "40m" or "70cm"
@@ -77,11 +79,11 @@ class Spot:
latitude: float = None latitude: float = None
longitude: float = None longitude: float = None
# Location source. Indicates how accurate the location might be. Values: "SPOT", "QRZ, "DXCC", "NONE" # Location source. Indicates how accurate the location might be. Values: "SPOT", "QRZ, "DXCC", "NONE"
location_source: str = None location_source: str = "NONE"
# Location good. Indicates that the software thinks the location data is good enough to plot on a map. # Location good. Indicates that the software thinks the location data is good enough to plot on a map.
location_good: bool = None location_good: bool = False
# QRT state. Some APIs return spots marked as QRT. Otherwise we can check the comments. # QRT state. Some APIs return spots marked as QRT. Otherwise we can check the comments.
qrt: bool = None qrt: bool = False
# Where we got the spot from, e.g. "POTA", "Cluster"... # Where we got the spot from, e.g. "POTA", "Cluster"...
source: str = None source: str = None
# The ID the source gave it, if any. # The ID the source gave it, if any.
@@ -118,11 +120,24 @@ class Spot:
band = infer_band_from_freq(self.freq) band = infer_band_from_freq(self.freq)
self.band = band.name self.band = band.name
# Mode from comments, mode family from mode # Mode from comments or bandplan
if self.mode:
self.mode_source = "SPOT"
if self.comment and not self.mode: if self.comment and not self.mode:
self.mode=infer_mode_from_comment(self.comment) self.mode = infer_mode_from_comment(self.comment)
if self.mode and not self.mode_family: self.mode_source = "COMMENT"
self.mode_family=infer_mode_family_from_mode(self.mode) if self.freq and not self.mode:
self.mode = infer_mode_from_frequency(self.freq)
self.mode_source = "BANDPLAN"
# Normalise "generic digital" modes. "DIGITAL", "DIGI" and "DATA" are just the same thing with no extra
# information, so standardise on "DATA"
if self.mode == "DIGI" or self.mode == "DIGITAL":
self.mode = "DATA"
# Mode type from mode
if self.mode and not self.mode_type:
self.mode_type = infer_mode_type_from_mode(self.mode)
# Grid to lat/lon and vice versa # Grid to lat/lon and vice versa
if self.grid and not self.latitude: if self.grid and not self.latitude:

View File

@@ -105,9 +105,9 @@ class WebServer:
case "mode": case "mode":
modes = query.get(k).split(",") modes = query.get(k).split(",")
spots = [s for s in spots if s.mode in modes] spots = [s for s in spots if s.mode in modes]
case "mode_family": case "mode_type":
mode_families = query.get(k).split(",") mode_families = query.get(k).split(",")
spots = [s for s in spots if s.mode_family in mode_families] spots = [s for s in spots if s.mode_type in mode_families]
case "dx_continent": case "dx_continent":
dxconts = query.get(k).split(",") dxconts = query.get(k).split(",")
spots = [s for s in spots if s.dx_continent in dxconts] spots = [s for s in spots if s.dx_continent in dxconts]

View File

@@ -122,7 +122,7 @@ paths:
- PSK - PSK
- BPSK31 - BPSK31
- OLIVIA - OLIVIA
- name: mode_family - name: mode_type
in: query in: query
description: "Limit the spots to only ones from one or more mode families. To select more than one mode family, supply a comma-separated list." description: "Limit the spots to only ones from one or more mode families. To select more than one mode family, supply a comma-separated list."
required: false required: false
@@ -338,7 +338,7 @@ components:
- BPSK31 - BPSK31
- OLIVIA - OLIVIA
example: SSB example: SSB
mode_family: mode_type:
type: string type: string
description: Inferred mode "family". description: Inferred mode "family".
enum: enum:
@@ -346,6 +346,14 @@ components:
- PHONE - PHONE
- DATA - DATA
example: PHONE example: PHONE
mode_source:
type: string
description: Where we got the mode from. If this was from the spot itself, it's likely quite accurate, but if we had to fall back to the bandplan, it might not be correct.
enum:
- SPOT
- COMMENT
- BANDPLAN
- NONE
freq: freq:
type: number type: number
description: Frequency, in kHz description: Frequency, in kHz

View File

@@ -3,16 +3,3 @@ div#table-container {
margin: 0 auto; margin: 0 auto;
overflow: scroll; overflow: scroll;
} }
div#table-container table, div#table-container th, div#table-container td {
border: 1px solid;
border-collapse: collapse
}
div#table-container th, div#table-container td {
padding: 10px;
}
div#table-container th {
background-color: dodgerblue;
}

View File

@@ -1,18 +1,40 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Unnamed spot tool</title> <title>Unnamed spot tool</title>
<link rel="stylesheet" href="css/style.css" type="text/css"> <link rel="stylesheet" href="css/style.css" type="text/css">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/css/bootstrap.min.css" rel="stylesheet"
integrity="sha384-sRIl4kxILFvY47J16cr9ZwB07vP4J8+LH7qKQnuqkuIAvNWLzeN8tE5YBujZqJLB" crossorigin="anonymous">
<script src="https://cdn.jsdelivr.net/npm/jquery@3.7.1/dist/jquery.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/jquery@3.7.1/dist/jquery.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/moment@2.29.4/moment.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/moment@2.29.4/moment.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/js/bootstrap.bundle.min.js"
integrity="sha384-FKyoEForCGlyvwx9Hj09JcYn3nv7wiPVlz7YYwJrWVcXK/BmnVDxM+D2scQbITxI"
crossorigin="anonymous"></script>
</head> </head>
<body> <body>
<h1>Unnamed spot tool</h1> <div class="container">
<div id="filters">Some filters here</div> <header class="d-flex flex-wrap justify-content-center py-3 mb-4 border-bottom">
<div id="table-container"></div> <a href="/" class="d-flex align-items-center mb-3 mb-md-0 me-md-auto text-dark text-decoration-none">
<span class="fs-4">Unnamed Spot Tool</span>
</a>
<script src="js/code.js"></script> <ul class="nav nav-pills">
<li class="nav-item"><a href="#" class="nav-link">About</a></li>
<li class="nav-item"><a href="/apidocs" class="nav-link">API</a></li>
<li class="nav-item"><a href="#" class="nav-link">Status</a></li>
<li class="nav-item"><a href="#" class="nav-link">Filters</a></li>
</ul>
</header>
</div>
<div id="table-container"></div>
<script src="js/code.js"></script>
</body> </body>
</html> </html>

View File

@@ -1,17 +1,19 @@
$.getJSON('/api/spots', function(jsonData) { $.getJSON('/api/spots', function(jsonData) {
let headers = Object.keys(jsonData[0]); let headers = Object.keys(jsonData[0]);
let table = $('<table>').append('<thead><tr></tr></thead><tbody></tbody>'); let table = $('<table class="table table-striped table-hover">').append('<thead><tr class="table-primary"></tr></thead><tbody></tbody>');
["Time", "DX", "Frequency", "Mode", "Comment", "Source", "DE"].forEach(header => table.find('thead tr').append(`<th>${header}</th>`)); ["Time", "DX", "Frequency", "Mode", "Comment", "Source", "DE"].forEach(header => table.find('thead tr').append(`<th>${header}</th>`));
jsonData.forEach(row => { jsonData.forEach(row => {
let $tr = $('<tr>'); let $tr = $('<tr>');
$tr.append(`<td>${row["time"]}</td>`); var time = moment(row["time"], moment.ISO_8601);
$tr.append(`<td>${row["dx_call"]}</td>`); var time_formatted = time.format("HH:mm")
$tr.append(`<td>${time_formatted}</td>`);
$tr.append(`<td>${row["dx_flag"]}&nbsp;${row["dx_call"]}</td>`);
$tr.append(`<td>${row["freq"]}</td>`); $tr.append(`<td>${row["freq"]}</td>`);
$tr.append(`<td>${row["mode"]}</td>`); $tr.append(`<td>${row["mode"]}</td>`);
$tr.append('<td>').append(escapeHtml(`${row["comment"]}`)).append('</td>'); $tr.append('<td>' + escapeHtml(`${row["comment"]}`) + '</td>');
$tr.append(`<td>${row["source"]}</td>`); $tr.append(`<td>${row["source"]}</td>`);
$tr.append(`<td>${row["de_call"]}</td>`); $tr.append(`<td>${row["de_flag"]}&nbsp;${row["de_call"]}</td>`);
table.find('tbody').append($tr); table.find('tbody').append($tr);
}); });