Implement spotting to Tiles on the Air. #95

This commit is contained in:
Ian Renton
2026-06-13 08:17:38 +01:00
parent 1afb407ca5
commit fd21e01c9d
13 changed files with 95 additions and 36 deletions

View File

@@ -1,6 +1,7 @@
import json
import logging
import re
import threading
from datetime import datetime
import pytz
@@ -175,8 +176,8 @@ class APISpotHandler(tornado.web.RequestHandler):
try:
# Submit spot to the upstream provider
provider.submit_spot(spot, upstream_credentials)
# Trigger an immediate re-poll so the spot appears quickly
provider.force_poll()
# Trigger a re-poll after 1 second so the spot appears quickly
threading.Timer(1.0, lambda: provider.force_poll()).start()
except NotImplementedError as e:
upstream_warning = str(e)
except Exception as e:

View File

@@ -21,7 +21,6 @@ class SOTA(HTTPSpotProvider):
SUMMIT_URL_ROOT = "https://api-db2.sota.org.uk/api/summits/"
SUBMIT_URL = "https://api-db2.sota.org.uk/api/spots"
VALID_MODES = ["AM", "CW", "Data", "DV", "FM", "SSB"]
def __init__(self, provider_config):
@@ -77,23 +76,24 @@ class SOTA(HTTPSpotProvider):
# Figure out a valid mode. Borrowed this from PoLo :)
# https://github.com/ham2k/app-polo/blob/main/src/extensions/activities/sota/SOTAPostSelfSpot.js
if spot.mode and spot.mode not in self.VALID_MODES:
if spot.mode in SSB_SUB_MODES:
spot.mode = "SSB"
elif spot.mode in DV_SUB_MODES:
spot.mode = "DV"
mode = spot.mode
if mode and mode not in self.VALID_MODES:
if mode in SSB_SUB_MODES:
mode = "SSB"
elif mode in DV_SUB_MODES:
mode = "DV"
else:
spot.mode = "Data"
mode = "Data"
body = {
"activatorCallsign": spot.dx_call,
"associationCode": ref_split[0],
"summitCode": ref_split[1],
"frequency": str(spot.freq / 1000000.0),
"mode": spot.mode or "",
"posterCallsign": spot.de_call,
"frequency": spot.freq / 1000000.0,
"mode": mode or "",
"callsign": spot.de_call,
"comments": spot.comment or "",
"type": "TEST" # todo remove once testing complete
"type": "TEST" # todo replatce with NORMAL/QRT once testing complete
}
headers = {**HTTP_HEADERS, "Authorization": "bearer " + access_token, "id_token": id_token, "Content-Type": "application/json"}
response = requests.post(self.SUBMIT_URL, json=body, headers=headers, timeout=(5, 30))

View File

@@ -1,5 +1,8 @@
from datetime import datetime
import requests
from core.constants import HTTP_HEADERS, SSB_SUB_MODES
from data.sig_ref import SIGRef
from data.spot import Spot
from spotproviders.http_spot_provider import HTTPSpotProvider
@@ -10,6 +13,8 @@ class Tiles(HTTPSpotProvider):
POLL_INTERVAL_SEC = 120
SPOTS_URL = "https://icneuzxitdqtofutxbla.supabase.co/functions/v1/spots?active_hours=24"
SUBMIT_URL = "https://icneuzxitdqtofutxbla.supabase.co/functions/v1/self-spot"
VALID_MODES = ["SSB", "CW", "FT8", "FT4", "FM", "DMR", "D-STAR", "M17", "AX.25", "JS8Call", "PSK31", "Olivia", "VarAC", "Other"]
def __init__(self, provider_config):
super().__init__(provider_config, self.SPOTS_URL, self.POLL_INTERVAL_SEC)
@@ -41,6 +46,48 @@ class Tiles(HTTPSpotProvider):
new_spots.append(spot)
return new_spots
def can_submit_spot(self, sig):
return sig == "Tiles"
def submit_spot(self, spot, credentials):
# Tiles on the air currently only supports *self* spots
if spot.dx_call == spot.de_call:
# Figure out a valid mode. Borrowed this from PoLo :)
# https://github.com/ham2k/app-polo/blob/main/src/extensions/activities/sota/SOTAPostSelfSpot.js
if spot.mode:
mode = spot.mode
if mode not in self.VALID_MODES:
if mode in SSB_SUB_MODES:
mode = "SSB"
elif mode == "OLIVIA":
mode = "Olivia"
elif mode == "JS8":
mode = "JS8Call"
else:
mode = "Other"
body = {
"call_sign": spot.dx_call,
"frequency": str(spot.freq / 1000000.0),
"mode": mode or "",
"grid": spot.dx_grid or "",
"comment": spot.comment or "",
"lat": spot.dx_latitude or None,
"lon": spot.dx_longitude or None,
"qrt": spot.qrt or False,
"pin": credentials.get("offline_spot_gateway_pin", "")
}
headers = {**HTTP_HEADERS, "Content-Type": "application/json"}
response = requests.post(self.SUBMIT_URL, json=body, headers=headers, timeout=(5, 30))
if not response.ok:
raise RuntimeError("Tiles on the Air API returned " + str(response.status_code) + ": " + response.text)
else:
raise RuntimeError("The Tiles on the Air API requires a mode to be set.")
else:
raise RuntimeError("The Tiles on the Air API only supports self-spots, the DX call and spotter call must match.")
# Utility function to keep the first decimal point in a given string but remove any others. Used to parse Tiles'
# strange frequency format where we can sometimes have e.g. "14.123.5".
def strip_extra_decimal_points(s):

View File

@@ -69,7 +69,7 @@
<p>This software is dedicated to the memory of Tom G1PJB, SK, a friend and colleague who sadly passed away around the time I started writing it in Autumn 2025. I was looking forward to showing it to you when it was done.</p>
</div>
<script src="/js/common.js?v=1781252061"></script>
<script src="/js/common.js?v=1781335058"></script>
<script>$(document).ready(function() { $("#nav-link-about").addClass("active"); }); <!-- highlight active page in nav --></script>
{% end %}

View File

@@ -109,8 +109,8 @@
<script>window._recaptchaSiteKey = {% raw json_encode(web_ui_options.get('recaptcha-site-key', '')) %};
window._allowUpstreamSpotting = {% raw json_encode(web_ui_options.get('allow-upstream-spotting', True)) %};</script>
<script src="/js/common.js?v=1781252061"></script>
<script src="/js/add-spot.js?v=1781252061"></script>
<script src="/js/common.js?v=1781335058"></script>
<script src="/js/add-spot.js?v=1781335058"></script>
<script>$(document).ready(function() { $("#nav-link-add-spot").addClass("active"); }); <!-- highlight active page in nav --></script>
{% end %}

View File

@@ -70,8 +70,8 @@
</div>
<script src="/js/common.js?v=1781252061"></script>
<script src="/js/alerts.js?v=1781252061"></script>
<script src="/js/common.js?v=1781335058"></script>
<script src="/js/alerts.js?v=1781335058"></script>
<script>$(document).ready(function() { $("#nav-link-alerts").addClass("active"); }); <!-- highlight active page in nav --></script>
{% end %}

View File

@@ -76,9 +76,9 @@
<script>
let spotProvidersEnabledByDefault = {% raw json_encode(web_ui_options["spot-providers-enabled-by-default"]) %};
</script>
<script src="/js/common.js?v=1781252061"></script>
<script src="/js/spotsbandsandmap.js?v=1781252061"></script>
<script src="/js/bands.js?v=1781252061"></script>
<script src="/js/common.js?v=1781335058"></script>
<script src="/js/spotsbandsandmap.js?v=1781335058"></script>
<script src="/js/bands.js?v=1781335058"></script>
<script>$(document).ready(function() { $("#nav-link-bands").addClass("active"); }); <!-- highlight active page in nav --></script>
{% end %}

View File

@@ -1,6 +1,6 @@
{% extends "skeleton.html" %}
{% block head_extra %}
<link rel="stylesheet" href="/css/style.css?v=1781252061" type="text/css">
<link rel="stylesheet" href="/css/style.css?v=1781335058" 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">
<link href="/fa/css/fontawesome.min.css" rel="stylesheet" />
@@ -19,9 +19,9 @@
integrity="sha384-L1eE4eD41kpBIWe2I0eHy+GnEUC4RIpcvibVW2JCminuPlTl+2Bc528iPdVMg5Dn"
crossorigin="anonymous"></script>
<script src="https://misc.ianrenton.com/jsutils/utils.js?v=1781252061"></script>
<script src="https://misc.ianrenton.com/jsutils/ui-ham.js?v=1781252061"></script>
<script src="https://misc.ianrenton.com/jsutils/geo.js?v=1781252061"></script>
<script src="https://misc.ianrenton.com/jsutils/utils.js?v=1781335058"></script>
<script src="https://misc.ianrenton.com/jsutils/ui-ham.js?v=1781335058"></script>
<script src="https://misc.ianrenton.com/jsutils/geo.js?v=1781335058"></script>
{% end %}
{% block body %}
<div class="container">

View File

@@ -284,8 +284,8 @@
</div>
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.9/dist/chart.umd.min.js"></script>
<script src="/js/common.js?v=1781252061"></script>
<script src="/js/conditions.js?v=1781252061"></script>
<script src="/js/common.js?v=1781335058"></script>
<script src="/js/conditions.js?v=1781335058"></script>
<script>$(document).ready(function () {
$("#nav-link-conditions").addClass("active");
}); <!-- highlight active page in nav --></script>

View File

@@ -94,9 +94,9 @@
<script>
let spotProvidersEnabledByDefault = {% raw json_encode(web_ui_options["spot-providers-enabled-by-default"]) %};
</script>
<script src="/js/common.js?v=1781252061"></script>
<script src="/js/spotsbandsandmap.js?v=1781252061"></script>
<script src="/js/map.js?v=1781252061"></script>
<script src="/js/common.js?v=1781335058"></script>
<script src="/js/spotsbandsandmap.js?v=1781335058"></script>
<script src="/js/map.js?v=1781335058"></script>
<script>$(document).ready(function() { $("#nav-link-map").addClass("active"); }); <!-- highlight active page in nav --></script>
{% end %}

View File

@@ -104,9 +104,9 @@
<script>
let spotProvidersEnabledByDefault = {% raw json_encode(web_ui_options["spot-providers-enabled-by-default"]) %};
</script>
<script src="/js/common.js?v=1781252061"></script>
<script src="/js/spotsbandsandmap.js?v=1781252061"></script>
<script src="/js/spots.js?v=1781252061"></script>
<script src="/js/common.js?v=1781335058"></script>
<script src="/js/spotsbandsandmap.js?v=1781335058"></script>
<script src="/js/spots.js?v=1781335058"></script>
<script>$(document).ready(function() { $("#nav-link-spots").addClass("active"); }); <!-- highlight active page in nav --></script>
{% end %}

View File

@@ -59,8 +59,8 @@
</div>
</div>
<script src="/js/common.js?v=1781252061"></script>
<script src="/js/status.js?v=1781252061"></script>
<script src="/js/common.js?v=1781335058"></script>
<script src="/js/status.js?v=1781335058"></script>
<script>
$(document).ready(function() { $("#nav-link-status").addClass("active"); }); <!-- highlight active page in nav -->
</script>

View File

@@ -15,6 +15,9 @@ var PROVIDER_CREDENTIAL_SCHEMAS = {
"ZLOTA": [
{ key: "user_id", label: "ZLOTA User ID", help: "" },
{ key: "api_key", label: "ZLOTA User PIN", help: "Get your PIN from your ZLOTA account." }
],
"Tiles": [
{ key: "offline_spot_gateway_pin", label: "Offline Spot Gateway PIN", help: "Get your PIN from your Tiles on the Air account profile." }
]
};
@@ -239,10 +242,18 @@ function addSpot() {
showAddSpotError("A SIG must be selected to submit upstream.");
return;
}
if (!sigRef) {
if (!sigRef && upstreamProviderName !== "Tiles") {
showAddSpotError("A SIG reference is required to submit upstream.");
return;
}
if (!dxGrid && upstreamProviderName === "Tiles") {
showAddSpotError("A grid reference is required to submit upstream to Tiles on the Air.");
return;
}
if (!mode && upstreamProviderName === "Tiles") {
showAddSpotError("A mode is required to submit upstream to Tiles on the Air.");
return;
}
var creds = loadCredentials(upstreamProviderName);
spot["submit_upstream"] = true;