from owrx.config.core import CoreConfig from owrx.config import Config from owrx.version import openwebrx_version from owrx.bookmarks import Bookmark import urllib import threading import logging import json import os import time import math logger = logging.getLogger(__name__) logger.setLevel(logging.DEBUG) # # Maximal distance a repeater can reach (kilometers) # MAX_DISTANCE = 200 class Repeaters(object): sharedInstance = None creationLock = threading.Lock() @staticmethod def getSharedInstance(): with Repeaters.creationLock: if Repeaters.sharedInstance is None: Repeaters.sharedInstance = Repeaters() return Repeaters.sharedInstance @staticmethod def _getCachedDatabaseFile(): coreConfig = CoreConfig() return "{data_directory}/repeaters.json".format(data_directory=coreConfig.get_data_directory()) # Compute distance, in kilometers, between two latlons. @staticmethod def distKm(p1, p2): # Earth radius in km earthR = 6371 # Convert degrees to radians rlat1 = p1[0] * (math.pi/180) rlat2 = p2[0] * (math.pi/180) # Compute difference in radians difflat = rlat2 - rlat1 difflon = (p2[1] - p1[1]) * (math.pi/180) # Compute distance return round(2 * earthR * math.asin(math.sqrt( math.sin(difflat/2) * math.sin(difflat/2) + math.cos(rlat1) * math.cos(rlat2) * math.sin(difflon/2) * math.sin(difflon/2) ))) # Guess main operating mode, prefer free modes @staticmethod def getModulation(entry): if "FM Analog" in entry and entry["FM Analog"]=="Yes": return "nfm" elif "M17" in entry and entry["M17"]=="Yes": return "m17" elif "DMR" in entry and entry["DMR"]=="Yes": return "dmr" elif "D-Star" in entry and entry["D-Star"]=="Yes": return "dstar" elif "System Fusion" in entry and entry["System Fusion"]=="Yes": return "ysf" elif "NXDN" in entry and entry["NXDN"]=="Yes": return "nxdn" else: return "nfm" def __init__(self): self.refreshPeriod = 60*60*24 self.lock = threading.Lock() self.repeaters = [] # Update repeater list when receiver location changes pm = Config.get() self.location = (pm["receiver_gps"]["lat"], pm["receiver_gps"]["lon"]) pm.wireProperty("receiver_gps", self._updateLocation) # Delete current repeater list when receiver location changes. def _updateLocation(self, location): location = (location["lat"], location["lon"]) file = self._getCachedDatabaseFile() dist = self.distKm(location, self.location) if not os.path.exists(file): # If there are no repeaters loaded, just keep new location self.location = location elif dist > 10: # Do not delete repeater list unless receiver moved a lot logger.debug("Receiver moved by {0}km, deleting '{1}'...".format(dist, file)) self.location = location os.remove(file) # # Load cached database or refresh it from the web. # def refresh(self): # This file contains cached database file = self._getCachedDatabaseFile() ts = os.path.getmtime(file) if os.path.isfile(file) else 0 # If cached database is stale... if time.time() - ts >= self.refreshPeriod: # Load EIBI database file from the web repeaters = self.loadFromWeb() if repeaters: # Save parsed data into a file self.saveRepeaters(file, repeaters) # Update current schedule with self.lock: self.repeaters = repeaters # If no current databse, load it from cached file if not self.repeaters: repeaters = self.loadRepeaters(file) with self.lock: self.repeaters = repeaters # # Save database to a given JSON file. # def saveRepeaters(self, file: str, repeaters): logger.debug("Saving {0} repeaters to '{1}'...".format(len(repeaters), file)) try: with open(file, "w") as f: json.dump(repeaters, f, indent=2) f.close() except Exception as e: logger.debug("saveRepeaters() exception: {0}".format(e)) # # Load database from a given JSON file. # def loadRepeaters(self, file: str): logger.debug("Loading repeaters from '{0}'...".format(file)) if not os.path.isfile(file): result = [] else: try: with open(file, "r") as f: result = json.load(f) f.close() except Exception as e: logger.debug("loadRepeaters() exception: {0}".format(e)) result = [] # Done logger.debug("Loaded {0} repeaters from '{1}'...".format(len(result), file)) return result # # Load repeater database from the RepeaterBook.com website. # def loadFromWeb(self, url: str = "https://www.repeaterbook.com/api/{script}?qtype=prox&dunit=km&lat={lat}&lng={lon}&dist={range}", rangeKm: int = MAX_DISTANCE): result = [] try: pm = Config.get() lat = pm["receiver_gps"]["lat"] lon = pm["receiver_gps"]["lon"] hdrs = { "User-Agent": "(OpenWebRX+, luarvique@gmail.com)" } # Start with US/Canada database for north-wester quartersphere if lat > 0 and lon < 0: scps = ["export.php", "exportROW.php"] else: scps = ["exportROW.php", "export.php"] # Try scripts in order... for s in scps: url1 = url.format(script = s, lat = lat, lon = lon, range = rangeKm) req = urllib.request.Request(url1, headers = hdrs) data = urllib.request.urlopen(req).read().decode("utf-8") logger.debug("Trying {0} ... got {1} bytes".format(url1, len(data))) data = json.loads(data) # ...until we get the result if "results" in data and len(data["results"]) > 0: break # If no results, do not continue if "results" not in data: return [] # For every entry in the response... for entry in data["results"]: result += [{ "name" : entry["Callsign"], "lat" : float(entry["Lat"]), "lon" : float(entry["Long"]), "freq" : int(float(entry["Frequency"]) * 1000000), "mode" : self.getModulation(entry), "status" : entry["Operational Status"], "updated" : entry["Last Update"], "comment" : entry["Notes"] }] except Exception as e: logger.debug("loadFromWeb() exception: {0}".format(e)) # Done return result # # Get bookmarks for all repeaters that are within given # frequency and distance ranges. # def getBookmarks(self, frequencyRange, rangeKm: int = MAX_DISTANCE): # Make sure freq2>freq1 (f1, f2) = frequencyRange if f1>f2: f = f1 f1 = f2 f2 = f # Get receiver location for computing distance pm = Config.get() rxPos = (pm["receiver_gps"]["lat"], pm["receiver_gps"]["lon"]) # No result yet logger.debug("Creating bookmarks for {0}-{1}kHz within {2}km...".format(f1//1000, f2//1000, rangeKm)) result = {} # Search for repeaters within frequency and distance ranges with self.lock: for entry in self.repeaters: try: f = entry["freq"] if f1 <= f <= f2: d = self.distKm(rxPos, (entry["lat"], entry["lon"])) if d <= rangeKm and (f not in result or d < result[f][1]): result[f] = (entry, d) except Exception as e: logger.debug("getBookmarks() exception: {0}".format(e)) # Return bookmarks for all found entries logger.debug("Created {0} bookmarks for {1}-{2}kHz within {3}km.".format(len(result), f1//1000, f2//1000, rangeKm)) return [ Bookmark({ "name" : result[f][0]["name"], "modulation" : result[f][0]["mode"], "frequency" : result[f][0]["freq"] }, srcFile = "RepeaterBook") for f in result.keys() ] # # Get entries for all repeaters that are within given distance # range from the receiver. # def getAllInRange(self, rangeKm: int = MAX_DISTANCE): # Get receiver location for computing distance pm = Config.get() rxPos = (pm["receiver_gps"]["lat"], pm["receiver_gps"]["lon"]) # No result yet logger.debug("Looking for repeaters within {0}km...".format(rangeKm)) result = [] # Search for repeaters within given distance range with self.lock: for entry in self.repeaters: try: if self.distKm(rxPos, (entry["lat"], entry["lon"])) <= rangeKm: result += [entry] except Exception as e: logger.debug("getAllInRange() exception: {0}".format(e)) # Done logger.debug("Found {0} repeaters within {1}km.".format(len(result), rangeKm)) return result