From fd21e01c9d03237be01eebff5840c7ee028f7359 Mon Sep 17 00:00:00 2001 From: Ian Renton Date: Sat, 13 Jun 2026 08:17:38 +0100 Subject: [PATCH] Implement spotting to Tiles on the Air. #95 --- server/handlers/api/addspot.py | 5 ++-- spotproviders/sota.py | 22 ++++++++-------- spotproviders/tiles.py | 47 ++++++++++++++++++++++++++++++++++ templates/about.html | 2 +- templates/add_spot.html | 4 +-- templates/alerts.html | 4 +-- templates/bands.html | 6 ++--- templates/base.html | 8 +++--- templates/conditions.html | 4 +-- templates/map.html | 6 ++--- templates/spots.html | 6 ++--- templates/status.html | 4 +-- webassets/js/add-spot.js | 13 +++++++++- 13 files changed, 95 insertions(+), 36 deletions(-) diff --git a/server/handlers/api/addspot.py b/server/handlers/api/addspot.py index 95509cd..f4e03ba 100644 --- a/server/handlers/api/addspot.py +++ b/server/handlers/api/addspot.py @@ -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: diff --git a/spotproviders/sota.py b/spotproviders/sota.py index f34d69d..15f8345 100644 --- a/spotproviders/sota.py +++ b/spotproviders/sota.py @@ -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)) diff --git a/spotproviders/tiles.py b/spotproviders/tiles.py index 406af8c..2779593 100644 --- a/spotproviders/tiles.py +++ b/spotproviders/tiles.py @@ -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): diff --git a/templates/about.html b/templates/about.html index 2672326..b36c287 100644 --- a/templates/about.html +++ b/templates/about.html @@ -69,7 +69,7 @@

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.

- + {% end %} \ No newline at end of file diff --git a/templates/add_spot.html b/templates/add_spot.html index 838f5b2..5661274 100644 --- a/templates/add_spot.html +++ b/templates/add_spot.html @@ -109,8 +109,8 @@ - - + + {% end %} diff --git a/templates/alerts.html b/templates/alerts.html index da63985..532c686 100644 --- a/templates/alerts.html +++ b/templates/alerts.html @@ -70,8 +70,8 @@ - - + + {% end %} \ No newline at end of file diff --git a/templates/bands.html b/templates/bands.html index 734b3f6..db6d520 100644 --- a/templates/bands.html +++ b/templates/bands.html @@ -76,9 +76,9 @@ - - - + + + {% end %} \ No newline at end of file diff --git a/templates/base.html b/templates/base.html index 24a863d..359b550 100644 --- a/templates/base.html +++ b/templates/base.html @@ -1,6 +1,6 @@ {% extends "skeleton.html" %} {% block head_extra %} - + @@ -19,9 +19,9 @@ integrity="sha384-L1eE4eD41kpBIWe2I0eHy+GnEUC4RIpcvibVW2JCminuPlTl+2Bc528iPdVMg5Dn" crossorigin="anonymous"> - - - + + + {% end %} {% block body %}
diff --git a/templates/conditions.html b/templates/conditions.html index 98b7204..81b9ac7 100644 --- a/templates/conditions.html +++ b/templates/conditions.html @@ -284,8 +284,8 @@
- - + + diff --git a/templates/map.html b/templates/map.html index 0d0727d..5644a47 100644 --- a/templates/map.html +++ b/templates/map.html @@ -94,9 +94,9 @@ - - - + + + {% end %} \ No newline at end of file diff --git a/templates/spots.html b/templates/spots.html index 7480c31..7e8c354 100644 --- a/templates/spots.html +++ b/templates/spots.html @@ -104,9 +104,9 @@ - - - + + + {% end %} \ No newline at end of file diff --git a/templates/status.html b/templates/status.html index ed5fbbb..143bd09 100644 --- a/templates/status.html +++ b/templates/status.html @@ -59,8 +59,8 @@ - - + + diff --git a/webassets/js/add-spot.js b/webassets/js/add-spot.js index 600b9eb..b668032 100644 --- a/webassets/js/add-spot.js +++ b/webassets/js/add-spot.js @@ -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;