#!/usr/bin/env python3 """ Meeluisteren met 112hier. Draait rtl_fm en multimon-ng, en stuurt de FLEX-regels die je antenne oppikt naar 112hier.nl. Daar worden ze samengevoegd met wat andere ontvangers horen, zodat er een landelijk beeld ontstaat in plaats van wat één antenne haalt. Wat je nodig hebt: - een RTL-SDR-stick met een antenne voor 169 MHz - rtl_fm en multimon-ng (apt install rtl-sdr multimon-ng) - python3, verder niets: dit script gebruikt alleen de standaardbibliotheek Aanmelden (eenmalig, geeft je sleutel): curl -X POST https://112hier.nl/meeluisteren/aanmelden \\ -H 'Content-Type: application/json' \\ -d '{"naam": "Zolder Roermond", "plaats": "Roermond"}' Starten: python3 meeluisteren.py --sleutel JOUW_SLEUTEL Je ontvanger staat eerst op proef: meldingen die alleen jij hoort worden nog niet gepubliceerd. Zodra je er genoeg hebt aangedragen die een ander ook hoorde, tellen die van jou volledig mee. Dat is er niet om jou te wantrouwen, maar om te voorkomen dat iemand verzonnen meldingen de site in duwt. Wat er verstuurd wordt: alleen de FLEX-regels zelf. Geen IP-adres dat wij bewaren, geen locatie behalve wat je bij het aanmelden zelf opgaf, en dat wordt afgerond op ongeveer een kilometer. MIT-licentie. Vragen of problemen: https://112hier.nl/meeluisteren """ import argparse import json import queue import signal import subprocess import sys import threading import time import urllib.error import urllib.request STANDAARD_URL = "https://112hier.nl/meeluisteren" FREQUENTIE = "169.6635M" # Regels worden gebundeld verstuurd: één verzoek per zoveel seconden, of eerder # als er genoeg regels klaarstaan. Dat scheelt verbindingen en is vriendelijker # voor de server dan elke regel apart. BUNDEL_SECONDEN = 10 BUNDEL_MAXIMUM = 100 class Verzender(threading.Thread): """Stuurt regels in bundels weg, en blijft draaien als het net eruit ligt.""" daemon = True def __init__(self, url, sleutel, stil=False): super().__init__() self.url = url self.sleutel = sleutel self.stil = stil self.rij = queue.Queue() self.stoppen = threading.Event() self.verstuurd = 0 self.bevestigd = 0 self.nieuw = 0 self.mislukt = 0 self.status = "onbekend" def run(self): while not self.stoppen.is_set(): bundel = self._verzamel() if bundel: self._stuur(bundel) def _verzamel(self): """Wacht tot er regels zijn, en pakt er dan zoveel als klaarstaan.""" bundel = [] einde = time.monotonic() + BUNDEL_SECONDEN while len(bundel) < BUNDEL_MAXIMUM and time.monotonic() < einde: try: bundel.append(self.rij.get(timeout=max(0.1, einde - time.monotonic()))) except queue.Empty: break if self.stoppen.is_set(): break return bundel def _stuur(self, regels): data = json.dumps({"sleutel": self.sleutel, "regels": regels}).encode() verzoek = urllib.request.Request( self.url, data=data, headers={"Content-Type": "application/json", "User-Agent": "112hier-meeluisteren/1.0"}) try: with urllib.request.urlopen(verzoek, timeout=20) as antwoord: uit = json.loads(antwoord.read().decode()) self.verstuurd += len(regels) self.bevestigd += uit.get("bevestigd", 0) self.nieuw += uit.get("nieuw", 0) vorige, self.status = self.status, uit.get("status", self.status) if vorige == "proef" and self.status == "vertrouwd": print("\n Je ontvanger telt nu volledig mee. Dank!\n", flush=True) if not self.stil: print(f" {len(regels):>3} regels verstuurd " f"(bevestigd {uit.get('bevestigd', 0)}, " f"nieuw {uit.get('nieuw', 0)}, status {self.status})", flush=True) except urllib.error.HTTPError as e: self.mislukt += len(regels) romp = e.read().decode(errors="replace")[:200] print(f" afgewezen ({e.code}): {romp}", file=sys.stderr, flush=True) if e.code in (401, 403): print(" Klopt je sleutel? Meld je opnieuw aan als je hem kwijt bent.", file=sys.stderr, flush=True) self.stoppen.set() except Exception as e: # netwerk eruit, DNS weg, server herstart self.mislukt += len(regels) # Niet opnieuw proberen: P2000 is een doorlopende stroom, en een # melding van vijf minuten geleden opnieuw aanbieden helpt niemand. print(f" versturen mislukt: {e}", file=sys.stderr, flush=True) def start_ontvanger(gain, ppm, apparaat): rtl = ["rtl_fm", "-f", FREQUENTIE, "-M", "fm", "-s", "22050"] if gain is not None: rtl += ["-g", str(gain)] if ppm: rtl += ["-p", str(ppm)] if apparaat is not None: rtl += ["-d", str(apparaat)] print(f"Starten: {' '.join(rtl)} | multimon-ng -a FLEX_NEXT -t raw -", flush=True) p1 = subprocess.Popen(rtl, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL) p2 = subprocess.Popen(["multimon-ng", "-a", "FLEX_NEXT", "-t", "raw", "-"], stdin=p1.stdout, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, text=True, bufsize=1) p1.stdout.close() # multimon krijgt de pijp; wij hoeven hem niet vast te houden return p1, p2 def main(): p = argparse.ArgumentParser(description="Meeluisteren met 112hier") p.add_argument("--sleutel", required=True, help="je ontvangersleutel") p.add_argument("--url", default=STANDAARD_URL, help="waar naartoe (standaard 112hier.nl)") p.add_argument("--gain", default=None, help="versterking van de stick, bv. 40.2") p.add_argument("--ppm", type=int, default=0, help="frequentiecorrectie in ppm") p.add_argument("--apparaat", default=None, help="welke stick, als je er meer hebt") p.add_argument("--stil", action="store_true", help="alleen fouten tonen") p.add_argument("--test", action="store_true", help="lees FLEX-regels van standaardinvoer in plaats van de stick") a = p.parse_args() verzender = Verzender(a.url, a.sleutel, a.stil) verzender.start() p1 = p2 = None if a.test: bron = sys.stdin else: p1, p2 = start_ontvanger(a.gain, a.ppm, a.apparaat) bron = p2.stdout def afsluiten(*_): print("\nStoppen...", flush=True) # Eerst versturen wat nog klaarstaat. Anders gooi je bij het afsluiten # de laatste tien seconden aan meldingen weg, en dat is precies het # moment waarop je herstart omdat er iets aan de hand is. einde = time.monotonic() + 15 while not verzender.rij.empty() and time.monotonic() < einde: time.sleep(0.2) verzender.stoppen.set() verzender.join(timeout=20) for proc in (p2, p1): if proc: try: proc.terminate() except Exception: pass print(f"Verstuurd: {verzender.verstuurd} regels, " f"bevestigd {verzender.bevestigd}, nieuw {verzender.nieuw}, " f"mislukt {verzender.mislukt}", flush=True) sys.exit(0) signal.signal(signal.SIGINT, afsluiten) signal.signal(signal.SIGTERM, afsluiten) gezien = 0 for regel in bron: regel = regel.strip() if not regel.startswith("FLEX"): continue gezien += 1 verzender.rij.put(regel) if verzender.stoppen.is_set(): break afsluiten() if __name__ == "__main__": main()