""" AP (a-priori) decoding for FT8 — targeted, not brute force. Recovers weak signals when the context (callsigns) is known. Three hypothesis sources: A) active QSO: pair (our_call, clicked_partner) B) someone is calling us: call_to = our_call C) band context: callsigns seen in previous windows """ import numpy as np import re from demod import extract_llr174 from ldpc_decode import bp_decode from unpack import unpack77, format_message import ft8_encoder as enc def _bits174(call_to, call_de, rpt): """Message -> 174 bits (via pack77 + crc14 + ldpc_encode).""" b = enc.pack77(call_to, call_de, rpt) c = enc._crc14(b + [0] * 5) a91 = b + list(c[:14]) return np.array(enc._ldpc_encode(np.array(a91[:91], dtype=np.int32))[:174]).astype(int) def _check_crc(hard): c = enc._crc14(list(hard[:77]) + [0] * 5) return list(c[:14]) == list(hard[77:91]) # most common reports in an FT8 QSO (order = probability priority) _COMMON_REPORTS = ['-01', '-05', '-10', '-15', 'R-01', 'R-05', 'R-10', 'RR73', '73', '+00', '-03', '-08', '-12', '-18'] def ap_try_candidate(power, hypotheses, prior_strength=6.0, max_iters=60): """ For a single candidate (its 'power' from extract_tone_power), tries the list of hypotheses (call_to, call_de). Returns (msg, i3) for the first one that decodes with a valid CRC and matches the hypothesis, or None. The report is iterated from _COMMON_REPORTS (since it's usually unknown). """ llr_ch = extract_llr174(power) for (to, de) in hypotheses: for rpt in _COMMON_REPORTS: try: full = _bits174(to, de, rpt) except Exception: continue prior = np.zeros(174) # prior only on the CALLSIGN bits [0:57] (leave the report to the channel) prior[:57] = np.where(full[:57] == 0, prior_strength, -prior_strength) hard, ok, it = bp_decode(llr_ch + prior, max_iters=max_iters) if not ok or not _check_crc(hard): continue msg = format_message(unpack77(list(hard[:77]))).strip() # VALIDATION: the decode must contain the hypothesized callsigns if de in msg and (to in msg or to == 'CQ'): return msg return None def build_band_context(decoded_messages): """ Mode C: extracts the set of callsigns (band context) from already decoded messages. """ calls = set() for msg in decoded_messages: for tok in msg.split(): if tok in ('CQ', 'DE', 'QRZ', 'RR73', 'RRR', '73'): continue if re.match(r'^R?[+-]?\d+$', tok): continue if re.match(r'^[A-R]{2}\d{2}([a-x]{2})?$', tok): continue if tok.startswith('<') or '...' in tok: continue calls.add(tok) return calls def make_hypotheses_band(known_calls, max_pairs=40): """ Mode C: builds a TARGETED list of pair hypotheses from known callsigns. NOT all pairs (that would time out) — prioritizes: 1. CQ from a known callsign (someone might be answering) 2. pairs where both callsigns are known (ongoing QSO) Caps at the max_pairs most plausible ones. """ calls = list(known_calls) hyps = [] # 1. CQ from each known callsign (someone calling a known callsign) for de in calls: hyps.append(('CQ', de)) # 2. pairs of known callsigns (reply in a QSO) — capped # priority: pairs that could plausibly be talking (heuristic: all of # them, but capped). In a full implementation: pairs from actually # observed QSOs. for i, de in enumerate(calls): for to in calls: if to == de: continue hyps.append((to, de)) if len(hyps) >= max_pairs: return hyps return hyps def make_hypotheses_qso(my_calls, partner): """ Mode A: active QSO. Very narrow, the safest mode. Pair (our_call, partner) in both directions. """ hyps = [] for my in my_calls: hyps.append((my, partner)) # we call the partner / they answer us hyps.append((partner, my)) # they call us return hyps def make_hypotheses_calling_us(my_calls): """ Mode B: someone is calling us. call_to = our callsign, call_de unknown. Returns hypotheses with our callsign as call_to (de='CQ' placeholder won't work here — this mode uses a prior on call_to [0:28] only). """ # this mode needs a different prior (call_to only) — handled separately return [(my, None) for my in my_calls]