FLARM FAMP: Empfänger-Software für das Kollisionswarnsystem

Ein reiner Empfänger-Codec für FLARM, das verbreitete Kollisionswarnsystem für Segelflieger, Ultraleicht- und andere Kleinflugzeuge — auf Basis der öffentlichen FAMP-Protokollspezifikation, mit Header-Parsing gegen alle offiziellen Testvektoren verifiziert.

FLARM ist ein Kollisionswarnsystem, das Segelflieger, Ultraleichtflugzeuge, Ballone und zunehmend auch Drohnen einsetzen: Jedes Gerät sendet regelmäßig seine eigene Position und Bewegungsdaten, jedes andere Gerät in Reichweite empfängt diese Daten und warnt bei drohenden Annäherungen. Das zugrunde liegende Funkprotokoll heißt FAMP (FLARM Aircraft Motion Prediction). Dieses Projekt implementiert einen Software-Decoder für FAMP-Pakete auf Basis der öffentlichen Spezifikation FTD-116-FLARM-FAMP-Public-Protocol-v1.01.pdf (FLARM Technology AG, 2026).

1. Rechtlicher Rahmen — nur Empfang, kein Senden

Die Spezifikation ist öffentlich einsehbar, ihre Lizenzbedingungen (Anhang 1, "License Terms and Conditions FFPP") schränken die Nutzung aber deutlich ein:

  • Erlaubt ist ausschließlich der Empfang ("limited to receive-only radio communication") zu privaten, Amateurfunk-, Forschungs- oder akademischen Zwecken, nicht-kommerziell.
  • Senden ist ausdrücklich ausgeschlossen. Ein eigener Sender bräuchte eine gesonderte, kommerzielle Lizenz von FLARM Technology inklusive Geräte-Validierung und Zuteilung eines eindeutigen Adressbereichs.
  • Der Grund dafür ist kein Formalismus: FLARM-Funkadressen müssen im Luftraum eindeutig sein. Ein unlizenzierter Sender mit einer bereits vergebenen oder kollidierenden Adresse kann echte Kollisionswarnungen stören — ein Sicherheitsrisiko für tatsächlichen Flugverkehr.
  • Zusätzlich gilt die allgemeine Funkregulierung im 868-MHz-SRD-Band (Short Range Device, lizenzfreier ISM-Frequenzbereich für Kurzstrecken-Funkanwendungen, z. B. ETSI EN 300 220 mit Vorgaben zu Sendeleistung, Duty-Cycle und Listen-Before-Talk).

Dieses Projekt enthält deshalb ausschließlich Empfänger-/Decoder-Software (reines Software-Parsing bereits empfangener Bytes) — keinen Sendecode für echte Funkhardware.

2. Protokoll-Aufbau

FLARM-Geräte senden auf 868,2 MHz und 868,4 MHz im Wechsel, moduliert mit 2-GFSK (binäre Frequenzumtastung mit vorgeschaltetem Gauß-Filter zur Bandbreitenbegrenzung) bei 100 kbit/s Rohrate. Sync-Word, Nutzdaten und Prüfsumme sind zusätzlich Manchester-codiert (jedes Datenbit wird als Flankenwechsel statt als Pegel übertragen, siehe Manchester-Code), was die effektive Rate auf 50 kbit/s halbiert. Jeder Sendezyklus dauert eine Sekunde und ist GNSS-zeitsynchronisiert, mit CSMA/LBT (Carrier Sense Multiple Access / Listen Before Talk — vor dem Senden wird der Kanal auf bereits laufende Übertragungen abgehört) zur Kollisionsvermeidung zwischen den Geräten.

Ein einzelnes Funkpaket besteht aus Preamble, einem 3-Byte-Sync-Word (0x31 0xFA 0xB6), 24 Byte Nutzlast und einer 16-Bit-Prüfsumme (CRC-16/CCITT, Polynom 0x1021, siehe CRC-Code). Die 24-Byte-Nutzlast gliedert sich weiter in:

  • 7 Byte Header (Funkadresse, Nachrichtentyp, Urgency-Stufe, Stealth-/No-Track-Flags) — unverschlüsselt, damit auch fremde Empfänger zumindest Adresse und Nachrichtentyp auswerten können.
  • 17 Byte FAMP-Nutzdaten — Versionsfelder, Zeitstempel, Flugzeugtyp, Position (komprimiert per Adaptive Coordinate Compression, ACC: die Koordinaten werden nicht absolut, sondern als kleine Differenz zu einer bekannten Referenzposition übertragen), Höhe/Geschwindigkeit/Steig-Sinkrate (Extended Range Encoding, ERC: eine Exponent-Mantisse-Kodierung, die große Wertebereiche in wenige Bit packt), Genauigkeits- und Integritätsangaben. Bis auf das erste Byte ist dieser Teil mit XXTEA verschlüsselt — einem kompakten, öffentlich dokumentierten Blockchiffre-Algorithmus. Der 128-Bit-Schlüssel dafür steht direkt in der Spezifikation; die Verschlüsselung dient laut Dokument der Manipulationssicherheit, nicht der Geheimhaltung gegenüber Empfängern.

3. Umsetzung

famp_codec.py ist ein reines Python-Modul ohne externe Abhängigkeiten und bildet die komplette Kette vom 24-Byte-Rohpaket bis zu den geparsten Feldern ab:

Baustein Aufgabe
crc_flarm* CRC-16/CCITT-Prüfsummenberechnung
erc_* Extended Range Encoding: Exponent+Mantisse-Dekodierung für Höhe/Geschwindigkeit
acc_* Adaptive Coordinate Compression: Positions-Dekompression relativ zur Empfänger-Referenzposition
mix32 EPRA-Adressmischung (eine AES-MixColumns-artige Funktion zur Adress-Verschleierung)
xxtea_encrypt/xxtea_decrypt XXTEA-Blockchiffre (Corrected Block TEA)
mix_nonce protokolleigene Nonce-Whitening-Funktion, aus Header-Bytes und Zeitstempel abgeleitet
parse_header, parse_famp_data, decode_packet Bit-Parsing von Header und Nutzdaten zum fertigen FampPacket

decode_example.py zeigt die minimale Verwendung:

from famp_codec import decode_packet

packet = bytes.fromhex("fb180d220000c003421c8b57f57738c20f29e561154 0a952".replace(" ", ""))
pkt = decode_packet(
    packet,
    ref_lat_raw=-273941486,   # eigene Empfänger-Position, 1E-7°, aus GNSS
    ref_lon_raw=-67004173,
    approx_unix_time=1996151268,  # eigene aktuelle UTC-Zeit (Unix-Sekunden)
)
print(pkt.header.radio_id.value(), pkt.header.address_type)
from famp_codec import decode_packet

packet = bytes.fromhex("fb180d220000c003421c8b57f57738c20f29e561154 0a952".replace(" ", ""))
pkt = decode_packet(
    packet,
    ref_lat_raw=-273941486,   # eigene Empfänger-Position, 1E-7°, aus GNSS
    ref_lon_raw=-67004173,
    approx_unix_time=1996151268,  # eigene aktuelle UTC-Zeit (Unix-Sekunden)
)
print(pkt.header.radio_id.value(), pkt.header.address_type)

decode_packet() erwartet ein bereits demoduliertes 24-Byte-Rohpaket — GFSK-Demodulation, Manchester-Decodierung, Sync-Word-Suche und CRC-Prüfung übernimmt der in Abschnitt 5 beschriebene GNU-Radio-Flowgraph.

4. Verifikationsstatus

test_famp_codec.py prüft den Codec gegen die zehn offiziellen Testvektoren aus Anhang 2 der Spezifikation:

Header + cleartext-byte checks: 10/10 passed
ERC round-trip: OK
ACC round-trip (compress -> decompress): OK
Payload decryption vs. Annex 2 (diagnostic, not asserted): 0/10 match
Header + cleartext-byte checks: 10/10 passed
ERC round-trip: OK
ACC round-trip (compress -> decompress): OK
Payload decryption vs. Annex 2 (diagnostic, not asserted): 0/10 match
  • Header- und Adress-Parsing: 10/10 Testvektoren korrekt, für alle vier Adressarten (FLARM, ICAO, EPRA, FLARM Extended).
  • ERC- und ACC-Kompressionsmathematik verifiziert, im Round-Trip gegen alle zehn Testvektoren exakt — inklusive der C-typischen Trunkierungs-Arithmetik, die bei negativen Referenzkoordinaten nötig ist (Python rundet ///% bei negativen Zahlen anders als C).
  • XXTEA und mix_nonce() je einzeln bit-exakt verifiziert: XXTEA gegen die unabhängige xxtea-Bibliothek, mix_nonce() gegen eine aus dem Spezifikations-Pseudocode kompilierte C-Referenzimplementierung.
  • Offener Punkt: Die Kombination aus beidem — die eigentliche Entschlüsselung der 16 verschlüsselten Nutzdaten-Bytes — reproduziert die erwarteten Klartextfelder (Zeit, Flugzeugtyp, Höhe, Position, Kinematik) noch nicht, obwohl jeder Baustein für sich genommen nachweislich korrekt arbeitet. Das deutet auf einen Fehler in der Verknüpfung hin (z. B. Wort-/Byte-Reihenfolge zwischen Nonce, Chiffretext und Schlüssel), nicht in den Einzelfunktionen.

Für einen reinen Adress-/Header-Empfänger (Zieltracking, Traffic-Zähler pro Adresstyp) ist der Codec damit bereits nutzbar; für die vollständige Positions- und Bewegungsdatenanzeige fehlt noch die Reparatur der Entschlüsselung.

5. Empfangs-Flowgraph

Der zweite, jetzt umgesetzte Teil ist die Funk-Schicht, die famp_codec.py bewusst ausklammert: aus dem rohen IQ-Signal ein CRC-geprüftes 24-Byte-Paket gewinnen. Dafür wurde ein separates Modul famp_radio_frame.py (Preamble, Manchester-Codierung, Sync-Word) sowie ein GNU-Radio-Flowgraph famp_rx.grc gebaut:

Quelle (IQ, complex64)
  → Throttle
  → GFSK Demod (Quadratur-Demod + Mueller-Muller-Taktrückgewinnung + Slicer)
  → FAMP Frame Decoder (Sync-Korrelation, Manchester-Decodierung, CRC-Prüfung, famp_codec.decode_packet())
  → Message Debug (Textausgabe)
Quelle (IQ, complex64)
  → Throttle
  → GFSK Demod (Quadratur-Demod + Mueller-Muller-Taktrückgewinnung + Slicer)
  → FAMP Frame Decoder (Sync-Korrelation, Manchester-Decodierung, CRC-Prüfung, famp_codec.decode_packet())
  → Message Debug (Textausgabe)

Warum diese Bausteine: Der GFSK Demod-Block ist GNU Radios eigene, fertige Hierarchie aus Quadraturdemodulation und Mueller-Muller-Taktrückgewinnung (ein Verfahren, das aus dem demodulierten Signal selbst den passenden Abtastzeitpunkt pro Symbol/Chip schätzt und nachführt, ohne einen separaten Taktkanal) — er liefert direkt den rohen Chip-Bitstrom (1 Bit je Ausgabebyte), ohne die für FLARM nicht zutreffende NRZI-Differenzdecodierung anderer GFSK-Varianten. Die eigentliche Frame-Erkennung ist in der Spezifikation nur als Prosa beschrieben (Preamble/Sync-Word "werden gesendet, damit der Empfänger sich synchronisiert"), nicht als Algorithmus — deshalb der eigene FAMP Frame Decoder-Block: Er korreliert den Chip-Strom gegen das feste 66-Chip-Muster aus Preamble + Manchester-codiertem Sync-Word.

Dimensionierung: Chiprate 100 kbit/s (FTD-116 2.1) mit 8 Samples/Symbol ergibt eine Abtastrate von 800 kHz — komfortabel über der spezifizierten 200-kHz-Kanalbandbreite. Die Demodulator-Sensitivity ergibt sich direkt aus dem spezifizierten Frequenzhub: 2π · 50 kHz / 800 kHz ≈ 0,393 rad/Sample. Die Sync-Korrelation toleriert bis zu 4 Bitfehler auf 66 Chips (~6 %), um bei realem Rauschen nicht sofort zu verwerfen, ohne bei zufälligem Rauschen falsch anzuschlagen (Wahrscheinlichkeit dafür astronomisch klein).

Erwartung: Ein synthetisch erzeugtes, aber mit GNU Radios eigenem digital.gfsk_mod (BT 0,5, ±50 kHz Hub, 100 kbit/s — dieselben Parameter wie beim Demodulator, keine Vereinfachung) moduliertes und verrauschtes Testsignal (gen_famp_testsignal.py, SNR ≈ 20 dB) sollte die komplette Kette bis zum korrekt decodierten FampPacket-Header durchlaufen; reines Rauschen sollte dagegen nie einen CRC-Treffer erzeugen.

Prüfweg und Messwerte: test_famp_rx_chain.py baut dieselbe Kette direkt in Python auf und prüft beide Fälle headless:

4 Nachricht(en):
  CRC OK  addr_type=FLARM radio_id=0d18fb msg_type=2 urgency=0 (...)
  CRC OK  addr_type=FLARM radio_id=0d18fb msg_type=2 urgency=0 (...)
  CRC OK  addr_type=FLARM radio_id=0d18fb msg_type=2 urgency=0 (...)
  CRC OK  addr_type=FLARM radio_id=0d18fb msg_type=2 urgency=0 (...)
OK: alle Pakete korrekt decodiert (Header stimmt mit decode_example.py ueberein).
OK: kein falsch-positiver Treffer in 20000 Rausch-Samples (0 CRC-Fehlversuche durch Sync-Fehltreffer, das ist erwartet).
4 Nachricht(en):
  CRC OK  addr_type=FLARM radio_id=0d18fb msg_type=2 urgency=0 (...)
  CRC OK  addr_type=FLARM radio_id=0d18fb msg_type=2 urgency=0 (...)
  CRC OK  addr_type=FLARM radio_id=0d18fb msg_type=2 urgency=0 (...)
  CRC OK  addr_type=FLARM radio_id=0d18fb msg_type=2 urgency=0 (...)
OK: alle Pakete korrekt decodiert (Header stimmt mit decode_example.py ueberein).
OK: kein falsch-positiver Treffer in 20000 Rausch-Samples (0 CRC-Fehlversuche durch Sync-Fehltreffer, das ist erwartet).

Alle vier im Testsignal enthaltenen Wiederholungen des Annex-2-Testpakets 0 werden korrekt decodiert — radio_id=0d18fb (hex) entspricht exakt der 858363 (dezimal), die decode_example.py unabhängig über famp_codec.decode_packet() direkt aus den Rohbytes ermittelt, ohne den Umweg über GFSK-Modulation/-Demodulation. Auf 20.000 Samples reinem Rauschen derselben Amplitude entsteht dagegen kein einziger falscher CRC-Treffer.

Grenze der Aussage: Verifiziert ist damit die Korrektheit der Funk-Rahmenschicht (Manchester-Codierung, Sync-Korrelation, CRC-Bytereihenfolge, GFSK-Parameter) an einem selbst erzeugten, aber spezifikationsgetreu parametrierten Signal — nicht an einer realen FLARM-Aufnahme, die hier nicht vorliegt. Für echten Empfang die blocks_file_source durch eine osmocom_source (gr-osmosdr, RTL-SDR/HackRF) ersetzen, abgestimmt auf 868,2/868,4 MHz; der Rest der Kette arbeitet unverändert auf dem Basisbandsignal weiter, unabhängig davon, wie es dorthin gelangt ist.

6. Praktischer Einsatz für Funkamateure

FLARM-Empfang ist ein etabliertes Amateurfunk-Betätigungsfeld, unabhängig von dieser Software-Implementierung: Das Open Glider Network (OGN) betreibt ein weltweites, von Freiwilligen aufgebautes Netz aus Bodenstationen, die FLARM- und ADS-B-Signale empfangen und zu einer Flugverfolgung ähnlich FlightRadar24 zusammenführen — allerdings speziell für Segelflieger, Ultraleicht- und andere General-Aviation-Flugzeuge, die auf ADS-B-Trackern seltener zu finden sind. Eine typische OGN-Empfangsstation besteht aus einem RTL-SDR-Dongle, einem Einplatinenrechner (z. B. Raspberry Pi oder Orange Pi Zero) und einer Kollinearantenne mit 5–9 dBi Gewinn, da FLARM-Signale mit ihrer geringen Sendeleistung 100- bis 1000-mal schwächer ankommen als typische ADS-B-Signale. Die dafür verwendete Software (ogn-rf/ogn-decode) übernimmt konzeptionell dieselben Schritte wie der in Abschnitt 5 beschriebene Flowgraph: GFSK-Demodulation, Sync-Word-Suche und CRC-Prüfung, bevor die eigentliche Paket-Dekodierung beginnt.

7. Nächste Schritte

  1. Den Entschlüsselungsfehler in decrypt_famp_payload() finden — am aussichtsreichsten über einen numerischen Soll/Ist-Vergleich: aus Chiffretext XOR erwarteter Klartext die erwartete Nonce für ein Testvektor-Feld zurückrechnen und mit der von mix_nonce() produzierten Nonce vergleichen.
  2. Den Empfangs-Flowgraph gegen eine echte RTL-SDR/HackRF-Quelle testen (siehe Abschnitt 5, „Grenze der Aussage") — inklusive Nachjustierung von Abtastrate/Filterung, falls die gewählte SDR-Sample-Rate nicht exakt 100 kbit/s · Samples/Symbol trifft.
  3. Optional eine einfache Kartendarstellung bzw. ein Log der empfangenen Ziele (Adresse, Typ, zuletzt gesehen) — ähnlich einem Mini-OGN-Empfänger.
  4. Sender/Transmit ist bewusst nicht geplant, siehe Abschnitt 1.

8. Download

Download: Codec, Funk-Rahmenschicht, Empfangs-Flowgraph und Tests als ZIP — flarm-famp.zip herunterladen, entpacken, dann:
python3 test_famp_codec.py       # prüft famp_codec.py gegen die 10 offiziellen Testvektoren
python3 decode_example.py        # decodiert ein Beispielpaket direkt (ohne Funk-Schicht)

python3 gen_famp_testsignal.py   # erzeugt ein synthetisches GFSK-IQ-Testsignal
python3 build_grc.py && grcc -o build famp_rx.grc && python3 build/famp_rx.py
# oder headless zur reinen Verifikation:
python3 test_famp_rx_chain.py
python3 test_famp_codec.py       # prüft famp_codec.py gegen die 10 offiziellen Testvektoren
python3 decode_example.py        # decodiert ein Beispielpaket direkt (ohne Funk-Schicht)

python3 gen_famp_testsignal.py   # erzeugt ein synthetisches GFSK-IQ-Testsignal
python3 build_grc.py && grcc -o build famp_rx.grc && python3 build/famp_rx.py
# oder headless zur reinen Verifikation:
python3 test_famp_rx_chain.py

Benötigt für den Codec-Teil nur Python-Standardbibliothek, für den Empfangs-Flowgraph zusätzlich GNU Radio 3.10 mit Python-Bindings. Die Referenzspezifikation FTD-116-FLARM-FAMP-Public-Protocol-v1.01.pdf ist wegen ihrer Lizenzbedingungen (siehe Abschnitt 1) nicht Teil des Downloads.