import logging import re import simplejson from pyhamtools.frequency import freq_to_band from pyhamtools.locator import latlong_to_locator from core.constants import BANDS, UNKNOWN_BAND from core.data_store import DATA_STORE from core.enums import MODE_ALIASES, Continent, Mode, ModeType from data.callsign import Callsign, LocationSourceForCallsign logger = logging.getLogger(__name__) def safe_json_dumps(obj): """Safe version of json.dumps that also converts objects to dicts so they can be output, and ignores NaN floats which are invalid in JSON.""" return simplejson.dumps(obj, ensure_ascii=False, ignore_nan=True, default=lambda o: o.__dict__) def infer_mode_from_comment(comment: str) -> Mode | None: """Infer a mode from the comment""" if not comment: return None for mode in Mode: if re.search(r"(^|\W)" + mode + r"($|\W)", comment, re.IGNORECASE): return mode for alias in MODE_ALIASES: if re.search(r"(^|\W)" + alias + r"($|\W)", comment, re.IGNORECASE): return Mode(MODE_ALIASES[alias]) return None def infer_mode_type_from_mode(mode: str) -> ModeType | None: """Infer a "mode family" from a mode .""" if not mode: return None if mode in MODE_ALIASES: mode = MODE_ALIASES[mode] try: mode = Mode(mode.upper()) if mode.is_cw: return ModeType.CW if mode.is_phone: return ModeType.PHONE return ModeType.DATA except ValueError: if mode.upper() != "OTHER" and mode != "?": logger.warning(f"Found an unrecognised mode: {mode}. Developer should categorise this.") return None def infer_band_from_freq(freq): """Infer a band from a frequency in Hz""" for b in BANDS: if b.start_freq <= freq <= b.end_freq: return b return UNKNOWN_BAND def infer_mode_from_frequency(freq): """Infer a mode from the frequency (in Hz) according to the band plan. Just a guess really.""" try: khz = freq / 1000.0 mode = freq_to_band(khz)["mode"] # Some additional common digimode ranges in addition to what the 3rd-party freq_to_band function returns. # This is mostly here just because freq_to_band is very specific about things like FT8 frequencies, and e.g. # a spot at 7074.5 kHz will be indicated as SSB, even though it's clearly in the FT8 range. Future updates # might include other common digimode centres of activity here, but this achieves the main goal of keeping # large numbers of clearly-FT* spots off the list of people filtering out digimodes. if ( (7074 <= khz < 7077) or (10136 <= khz < 10139) or (14074 <= khz < 14077) or (18100 <= khz < 18103) or (21074 <= khz < 21077) or (24915 <= khz < 24918) or (28074 <= khz < 28077) ): mode = "FT8" if ( (7047.5 <= khz < 7050.5) or (10140 <= khz < 10143) or (14080 <= khz < 14083) or (18104 <= khz < 18107) or (21140 <= khz < 21143) or (24919 <= khz < 24922) or (28180 <= khz < 28183) ): mode = "FT4" return mode except KeyError: return None def get_flag_for_dxcc(dxcc): """Get an emoji flag for a given DXCC entity ID""" dxcc_data = DATA_STORE.dxcc_data.get(dxcc, None) return dxcc_data["flag"] if dxcc_data else None def get_callsign_object_from_pyhamtools_callinfo(callsign, callinfo): """Utility function to take the data provided by a PyHamTools CallInfo object and populate our own Callsign data object from it""" try: home_call = callinfo.get_homecall(callsign) data = callinfo.get_all(callsign) country = data.get("country", None) dxcc_id = data.get("adif", None) continent = Continent(data["continent"]) if "continent" in data else None cq_zone = data.get("cqz", None) itu_zone = data.get("ituz", None) lat = float(data["latitude"]) if "latitude" in data else None lon = float(data["longitude"]) if "longitude" in data else None grid = None if lat and lon: grid = latlong_to_locator(lat, lon) return Callsign( call=callsign, home_call=home_call, country=country, dxcc_id=dxcc_id, continent=continent, cq_zone=cq_zone, itu_zone=itu_zone, latitude=lat, longitude=lon, grid=grid, location_source=LocationSourceForCallsign.DXCC, ) except (KeyError, ValueError): # Unknown callsign, can't look anything up, return a Callsign object with basic data so that gets cached return Callsign(call=callsign)