diff --git a/Dockerfile b/Dockerfile index a775212..0f5a15a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -13,6 +13,8 @@ RUN set -x && \ TEMP_PACKAGES+=(libjemalloc-dev) && \ # keep KEPT_PACKAGES+=(libtiff6) && \ + KEPT_PACKAGES+=(python3) && \ + KEPT_PACKAGES+=(python3-prctl) && \ apt-get update && \ apt-get install -y --no-install-recommends \ "${KEPT_PACKAGES[@]}" \ diff --git a/README.md b/README.md index 217a56c..ef89dd3 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,8 @@ A Docker image with satdump installed. Currently it runs an arbitrary command on startup, but that will change in the future. +There is a python script also run at startup which takes the satdump UDP JSON messages and reformats them as acarshub compatible JSON. + Under active development, everything is subject to change without notice. You can view only ACARS message in the log with: @@ -19,6 +21,7 @@ docker logs -f satdump | grep -v "(D)" | grep -v "Table Broadcast" | grep -v "Re | Variable | Description | Default | |----------|-------------|---------| | `RUN_CMD` | The command to run when the container starts. The container will restart when it returns. | Unset | +| `JSON_OUT` | The `host:port` to forward reformatted JSON messages to. | `acars_router:5550` | ## Docker Compose @@ -38,8 +41,37 @@ services: - ./vfo.json:/vfo.json - ./Inmarsat.json:/usr/share/satdump/pipelines/Inmarsat.json environment: - - RUN_CMD=satdump live inmarsat_aero_6 /tmp/satdump_out --source rtltcp --ip_address 10.0.0.114 --port 7373 --gain 49 --samplerate 1.536e6 --frequency 1545.552e6 --multi_vfo /vfo.json 2>&1 | grep -v "Invalid CRC!" -# - RUN_CMD=satdump live inmarsat_aero_6 /tmp/satdump_out --source rtlsdr --source_id 0 --gain 49 --samplerate 1.536e6 --frequency 1545.552e6 --multi_vfo /vfo.json 2>&1 | grep -v "Invalid CRC!" + - RUN_CMD=satdump live inmarsat_aero_6 /tmp/satdump_out --source rtltcp --ip_address 10.0.0.114 --port 7373 --gain 49 --samplerate 1.536e6 --frequency 1545.6e6 --multi_vfo /vfo.json 2>&1 | grep -v "Invalid CRC!" +# - RUN_CMD=satdump live inmarsat_aero_6 /tmp/satdump_out --source rtlsdr --source_id 0 --gain 49 --samplerate 1.536e6 --frequency 1545.6e6 --multi_vfo /vfo.json 2>&1 | grep -v "Invalid CRC!" + + acarshubsat: + image: ghcr.io/sdr-enthusiasts/docker-acarshub:latest + container_name: acarshubsat + restart: always + ports: + - 8000:80 + tmpfs: + - /database:exec,size=64M + - /run:exec,size=64M + - /var/log:size=64M + environment: + - TZ=America/New_York + - ENABLE_ACARS=external + - MIN_LOG_LEVEL=3 + + acars_router: + image: ghcr.io/sdr-enthusiasts/acars_router:latest + container_name: acars_router + restart: always + environment: + - TZ=America/New_York + - AR_SEND_UDP_ACARS=acarshubsat:5550 + tmpfs: + - /run:exec,size=64M + - /var/log + + + ``` The above setup is intended to decode Inmarsat 4F3 98W from an rtl_tcp stream at 10.0.0.114:7373. To directly use an RTL-SDR instead, uncomment the `cgroup` and `/dev` lines and switch which `RUN_CMD` line is commented. You may need to change the `--source_id` if you have more than one RTL-SDR. @@ -48,74 +80,98 @@ The above setup is intended to decode Inmarsat 4F3 98W from an rtl_tcp stream at ``` { - "vfo1": { - "frequency": 1545023110, - "pipeline": "inmarsat_aero_6" - }, - "vfo2": { - "frequency": 1545053110, - "pipeline": "inmarsat_aero_6" - }, - "vfo3": { - "frequency": 1545063110, - "pipeline": "inmarsat_aero_6" - }, - "vfo4": { - "frequency": 1545068110, - "pipeline": "inmarsat_aero_6" - }, - "vfo5": { - "frequency": 1545078260, - "pipeline": "inmarsat_aero_12" - }, - "vfo6": { - "frequency": 1545083110, - "pipeline": "inmarsat_aero_6" - }, - "vfo7": { - "frequency": 1545088110, - "pipeline": "inmarsat_aero_6" - }, - "vfo8": { - "frequency": 1545093110, - "pipeline": "inmarsat_aero_6" - }, - "vfo9": { - "frequency": 1545103110, - "pipeline": "inmarsat_aero_6" - }, - "vfo10": { - "frequency": 1545113110, - "pipeline": "inmarsat_aero_6" - }, - "vfo11": { - "frequency": 1545173110, - "pipeline": "inmarsat_aero_6" - }, - "vfo12": { - "frequency": 1545178110, - "pipeline": "inmarsat_aero_6" - }, - "vfo13": { - "frequency": 1545208110, - "pipeline": "inmarsat_aero_6" - }, - "vfo14": { - "frequency": 1546008660, - "pipeline": "inmarsat_aero_105" - }, - "vfo15": { - "frequency": 1546023660, - "pipeline": "inmarsat_aero_105" - }, - "vfo16": { - "frequency": 1546066110, - "pipeline": "inmarsat_aero_105" - }, - "vfo17": { - "frequency": 1546081660, - "pipeline": "inmarsat_aero_105" - } + "vfo1": { + "frequency": 1545018800, + "pipeline": "inmarsat_aero_6" + }, + "vfo2": { + "frequency": 1545023800, + "pipeline": "inmarsat_aero_6" + }, + "vfo3": { + "frequency": 1545028800, + "pipeline": "inmarsat_aero_6" + }, + "vfo4": { + "frequency": 1545033800, + "pipeline": "inmarsat_aero_6" + }, + "vfo5": { + "frequency": 1545038800, + "pipeline": "inmarsat_aero_6" + }, + "vfo6": { + "frequency": 1545043800, + "pipeline": "inmarsat_aero_6" + }, + "vfo7": { + "frequency": 1545053800, + "pipeline": "inmarsat_aero_6" + }, + "vfo8": { + "frequency": 1545063800, + "pipeline": "inmarsat_aero_6" + }, + "vfo9": { + "frequency": 1545068800, + "pipeline": "inmarsat_aero_6" + }, + "vfo10": { + "frequency": 1545078800, + "pipeline": "inmarsat_aero_12" + }, + "vfo11": { + "frequency": 1545083800, + "pipeline": "inmarsat_aero_6" + }, + "vfo12": { + "frequency": 1545088800, + "pipeline": "inmarsat_aero_6" + }, + "vfo13": { + "frequency": 1545093800, + "pipeline": "inmarsat_aero_6" + }, + "vfo14": { + "frequency": 1545103800, + "pipeline": "inmarsat_aero_6" + }, + "vfo15": { + "frequency": 1545113800, + "pipeline": "inmarsat_aero_6" + }, + "vfo16": { + "frequency": 1545173800, + "pipeline": "inmarsat_aero_6" + }, + "vfo18": { + "frequency": 1545178800, + "pipeline": "inmarsat_aero_6" + }, + "vfo19": { + "frequency": 1545198800, + "pipeline": "inmarsat_aero_6" + }, + "vfo20": { + "frequency": 1545208800, + "pipeline": "inmarsat_aero_6" + }, + "vfo21": { + "frequency": 1546008800, + "pipeline": "inmarsat_aero_105" + }, + "vfo22": { + "frequency": 1546023800, + "pipeline": "inmarsat_aero_105" + }, + "vfo23": { + "frequency": 1546066300, + "pipeline": "inmarsat_aero_105" + }, + "vfo24": { + "frequency": 1546081300, + "pipeline": "inmarsat_aero_105" + } } ``` @@ -158,10 +214,10 @@ The above setup is intended to decode Inmarsat 4F3 98W from an rtl_tcp stream at "msg": { "inmarsat_stdc_parser": { "save_files": false, - "station_id": "XX-YYYY-IMSL-98W-STDC", + "station_id": "XX-YYY-IMSL-98W-STDC", "udp_sinks": { "test": { - "address": "10.0.0.114", + "address": "127.0.0.1", "port": 5556 } } @@ -206,10 +262,10 @@ The above setup is intended to decode Inmarsat 4F3 98W from an rtl_tcp stream at "msg": { "inmarsat_aero_parser": { "save_files": false, - "station_id": "XX-YYYY-IMSL-98W-AERO6", + "station_id": "XX-YYY-IMSL-98W-AERO6", "udp_sinks": { "test": { - "address": "10.0.0.114", + "address": "127.0.0.1", "port": 5556 } } @@ -253,10 +309,10 @@ The above setup is intended to decode Inmarsat 4F3 98W from an rtl_tcp stream at "msg": { "inmarsat_aero_parser": { "save_files": false, - "station_id": "XX-YYYY-IMSL-98W-AERO12", + "station_id": "XX-YYY-IMSL-98W-AERO12", "udp_sinks": { "test": { - "address": "10.0.0.114", + "address": "127.0.0.1", "port": 5556 } } @@ -303,10 +359,10 @@ The above setup is intended to decode Inmarsat 4F3 98W from an rtl_tcp stream at "msg": { "inmarsat_aero_parser": { "save_files": false, - "station_id": "XX-YYYY-IMSL-98W-AERO105", + "station_id": "XX-YYY-IMSL-98W-AERO105", "udp_sinks": { "test": { - "address": "10.0.0.114", + "address": "127.0.0.1", "port": 5556 } } @@ -357,10 +413,10 @@ The above setup is intended to decode Inmarsat 4F3 98W from an rtl_tcp stream at "inmarsat_aero_parser": { "is_c": true, "save_files": false, - "station_id": "XX-YYYY-IMSL-98W-AERO84", + "station_id": "XX-YYY-IMSL-98W-AERO84", "udp_sinks": { "test": { - "address": "10.0.0.114", + "address": "127.0.0.1", "port": 5556 } } diff --git a/rootfs/etc/s6-overlay/s6-rc.d/reformat/run b/rootfs/etc/s6-overlay/s6-rc.d/reformat/run new file mode 100644 index 0000000..70560e4 --- /dev/null +++ b/rootfs/etc/s6-overlay/s6-rc.d/reformat/run @@ -0,0 +1,7 @@ +#!/command/with-contenv bash +#shellcheck shell=bash + +# shellcheck disable=SC1091 +#sleep 86400 +echo "starting JSON reformatter" +exec python3 -u /etc/scripts/reformat.py 2>&1 diff --git a/rootfs/etc/s6-overlay/s6-rc.d/reformat/type b/rootfs/etc/s6-overlay/s6-rc.d/reformat/type new file mode 100644 index 0000000..5883cff --- /dev/null +++ b/rootfs/etc/s6-overlay/s6-rc.d/reformat/type @@ -0,0 +1 @@ +longrun diff --git a/rootfs/etc/s6-overlay/s6-rc.d/user/contents.d/reformat b/rootfs/etc/s6-overlay/s6-rc.d/user/contents.d/reformat new file mode 100644 index 0000000..e69de29 diff --git a/rootfs/etc/scripts/reformat.py b/rootfs/etc/scripts/reformat.py new file mode 100644 index 0000000..a6fcfde --- /dev/null +++ b/rootfs/etc/scripts/reformat.py @@ -0,0 +1,305 @@ +from re import compile +import locale +import socket +from sys import stderr +import traceback +from datetime import datetime, timezone +from json import loads, dumps +from os import getenv +from pprint import pprint +import prctl +from queue import SimpleQueue +from threading import Thread, current_thread +from time import sleep + +#from colorama import Fore + +def rx_thread(port, rxq): + prctl.set_name(f"rx {port}") + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.bind(('', port)) +# rdr = sock2lines(sock) + print(f"Connected to JSON input on port {port}") + while True: +# msg = next(rdr).strip() + msg = sock.recv(65536) + if msg: + rxq.put_nowait(msg) + else: + sleep(1) + +def tx_thread(host, txq): + prctl.set_name(f"tx {host[0]}:{host[1]}") + enc = locale.getpreferredencoding(False) +# sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock.connect(host) + print(f"Connected to JSON output at {host[0]}:{host[1]}") + while True: + msg = txq.get() + sock.sendall(msg.encode(enc)) +# sock.sendto(msg.encode(enc), host) + +# wrapper to catch exceptions and restart threads +def thread_wrapper(func, *args): + slp = 10 + while True: + try: + print(f"[{current_thread().name}] starting thread") + func(*args) + except BrokenPipeError: + print(f"[{current_thread().name}] pipe broken; restarting thread in {slp} seconds") + except ConnectionRefusedError: + print(f"[{current_thread().name}] connection refused; restarting thread in {slp} seconds") + except StopIteration: + print(f"[{current_thread().name}] lost connection; restarting thread in {slp} seconds") + except BaseException as exc: + print(traceback.format_exc()) + print(f"[{current_thread().name}] exception {type(exc).__name__}; restarting thread in {slp} seconds") + else: + print(f"[{current_thread().name}] thread function returned; restarting thread in {slp} seconds") + sleep(slp) + +gesLoc = { + "ANCXFXA": "Anchorage Domestic (USA)", + "ANCATYA": "Anchorage Oceanic (USA)", + "AKLCDYA": "Auckland Oceanic (NZ)", + "BKKGWXA": "Bangkok (Thailand)", + "BNECAYA": "Brisbane (AUS)", + "CTUGWYA": "Chengdu (China)", + "MAACAYA": "Chennai (India)", + "FUKJJYA": "Fukuoka (Japan)", + "YQXE2YA": "Gander Oceanic (Canada)", + "YQXD2YA": "Gander Domestic (Canada)", + "YQME2YA": "Moncton (Canada)", + "YULE2YA": "Montreal (Canada)", + "YYZE2YA": "Toronto (Canada)", + "YWGE2YA": "Winnipeg (Canada)", + "YVRE2YA": "Vancouver (Canada)", + "USADCXA": "USA Domestic (USA)", + "JNBCAYA": "Johannesburg Oceanic (SA)", + "KMGGWYA": "Kunming (China)", + "LHWGWYA": "Lanzhou (China)", + "MELCAYA": "Melbourne (AUS)", + "BOMCAYA": "Mumbai (India)", + "NANCDYA": "Nadi (Fiji)", + "NYCODYA": "New York Oceanic (USA)", + "OAKODYA": "Oakland Oceanic (USA)", + "REKCAYA": "Reykjavik (Iceland)", + "SMACAYA": "Santa Maria (Portugal)", + "PIKCPYA": "Shanwick (UK)", + "SINCDYA": "Singapore", + "PPTCDYA": "Tahiti (French Polynesia)", + "UPGCAYA": "Makassar (Indonesia)", + "NIMCAYA": "Niamey (Niger)", + "DKRCAYA": "Dakar Oceanic (Senegal)", + "NKCCAYA": "Dakar Domestic (Senegal)", + "GVSCAYA": "Sal Oceanic (Cape Verde)", + "BZVCAYA": "Brazzaville (Congo)", + "CAICAYA": "Egypt", + "PAREUYA": "France", + "MEXCAYA": "Mexico", + "BKKCAYA": "Thailand", + "NDJCAYA": "Ndjamena (Chad)", + "LPAFAYA": "Canarias (Spain)", + "ALGCAYA": "Alger (Algeria)", + "SEZCAYA": "Seychelles", + "LADCAYA": "Angola (Luanda)", + "ABJCAYA": "Ivory Coast (Abidjan)", + "TNRCAYA": "Antananarivo (Madagascar)", + "KRTCAYA": "Khartoum (Sudan)", + "CCUCAYA": "Kolkata (India)", + "SNNCPXA": "Shannon (Ireland)", + "PIKCAYA": "Scottish (UK)", + "SOUCAYA": "London (UK)", + "MSTEC7X": "Maastricht (NL)", + "POSCLYA": "Piarco (Trinidad)", + "TGUACYA": "Cenamar (Honduras)", + "CAYCAYA": "Cayenne (French Guiana)", + "RECOEYA": "Atlantico (Brazil)", + "SCLCAYA": "Antofagasta (Chile)", + "GDXE1XA": "Magadan (Russia)", + "URCE1YA": "Urumqi (China)", + "RPHIAYA": "Manila (Philippines)", + "SGNGWXA": "Ho Chi Minh (Vietnam)", + "RGNCAYA": "Yangon (Myanmar)", + "SINCXYA": "Singapore", + "KULCAYA": "Kuala Lumpur (Malaysia)", + "POMCAYA": "Port Moresby (PG)", + "DELCAYA": "Delhi (India)", + "CMBCBYA": "Colombo (Sri Lanka)", + "MRUCAYA": "Mauritius (Mauritius)", + "TGUACAY": "Honduras (CENAMER)", + "MLECAYA": "Male (Maldives)", + "BDOCAYA": "Bodo Oceanic (Norway)", + "NBOCAYA": "Kenya (Niarobi)", + "ACCFAYA": "Gahna (Accra)", + "BJSGWYA": "Beijing (China)", + "CAICDYA": "Cairo (Egypt)", + "CANGWYA": "Guangzhou (China)", + "CCUCBYA": "Kolkata (India)", + "CMBCAYA": "Colombo (Sri Lanka)", + "DDLCVXA": "Bodo (Norway)", + "GDXGWXA": "Magadan (Russia)", + "HKGCCYA": "Hong Kong", + "HRBGWYA": "Harbin (China)", + "JAKGWXA": "Jakarta (Indonesia)", + "KANCAYA": "Kano (Nigeria)", + "KRTCDYA": "Sudan", + "LISACYA": "Lisboa (Portugal)", + "LXAGWYA": "Lhasa (China)", + "MNLCBYA": "Manila (Philippines)", + "SELCAXH": "Seoul (Korea)", + "SHAGWYA": "Shanghai (China)", + "TASCAXH": "Tashkent (Uzbekistan)", + "ULNGWXA": "Ulan Bataar (Mongolia)", + "URCGWYA": "Urum-Qi (China)", + "YEGCDYA": "Edmonton (Canada)", + "YEGE2YA": "Edmonton (Canada)", + "SPLATYA": "Amsterdam Schipol (NL)", + "CDGATYA": "Paris (France)", + "MGQCAYA": "Ethiopia", + "FIHCAYA": "Congo", + "PIKCLYA": "Prestwick Oceanic (UK)", + "PIKCLXS": "Prestwick Domestic (UK)", + "DOHATYA": "Doha (Qatar)", + "MCTASWY": "Muscat (Oman)", + "JEDAAYA": " ", + "DXBEGEK": " ", + "RUHAAYA": " ", + "BOMCDYA": " ", + "BJSATYA": " ", + "MADAAYA": " ", + "JEDATYA": " ", + "GVACBYA": " ", + "DATSAXS": " ", + "AUHCAYA": " ", + "BAHCEYA": " ", + "MADCDYA": " ", + "AUHABYA": " ", + "GYDCDYA": " ", + "HDQOILX": " ", + "RIOCDYA": " ", +} + +json_in = getenv("UDP_IN", "5556") +json_in = json_in.split(";") +json_in = [int(x) for x in json_in] + +sbs_out = getenv("JSON_OUT", "acars_router:5550") +#sbs_out = getenv("JSON_OUT", "10.0.0.109:5559") +sbs_out = sbs_out.split(";") +sbs_out = [x.split(":") for x in sbs_out] +sbs_out = [(x,int(y)) for x,y in sbs_out] + +rxq = SimpleQueue() +for p in json_in: + Thread(name=f"rx {p}", target=thread_wrapper, args=(rx_thread, p, rxq)).start() + +txqs = [] +for i,s in enumerate(sbs_out): + txqs.append(SimpleQueue()) + Thread(name=f"tx {s[0]}:{s[1]}", target=thread_wrapper, args=(tx_thread, s, txqs[-1])).start() + +fn1 = compile(r"\/FN(?P\w+)\/") +fn2 = compile(r"\/FMH(?P\w+),") + +gs1 = compile(r"^\/(?P\w{7})\.") + +while True: + try: + raw = rxq.get() +# print(f"{raw}\n") + + data = loads(raw) + if not data or "ACARS" != data.get("msg_name"): + continue + + flight = "" + fl1 = fn1.search(data.get("message", "")) + if fl1: + flight = fl1.groupdict().get("fn") + if not flight: + fl2 = fn2.search(data.get("message", "")) + if fl2: + flight = fl2.groupdict().get("fn") + + if not flight or len(flight) > 9: + flight = "" + + gsa = data.get("libacars", {}).get("arinc622", {}).get("gs_addr", "") + if not gsa: + ges1 = gs1.search(data.get("message", "")) + if ges1: + gsa = ges1.groupdict().get("gs") + from_decoded = f"{gsa}/{gesLoc.get(gsa, '')}" + + out = { + "freq": 1545.0 if "6" in data.get("source").get("station_id", "") else 1545.075 if "12" in data.get("source").get("station_id", "") else 1546, + "channel": 0, + "error": 0, + "level": 0.0, + "timestamp": data.get("timestamp"), + "app": { + "name": data.get("source", {}).get("app", {}).get("name", ""), + "ver": data.get("source", {}).get("app", {}).get("version", "") + }, + "station_id": data.get("source").get("station_id", ""), + "icao": data.get("signal_unit", {}).get("aes_id", ""), + "toaddr": data.get("signal_unit", {}).get("aes_id", ""), + "mode": str(data.get("mode", "")), + "label": data.get("label", ""), + "block_id": str(data.get("bi", "")), + "ack": "", + "tail": data.get("plane_reg[1:]", ""), + "text": data.get("message", ""), + "msgno": str(data.get("signal_unit", {}).get("ref_no", "")), + "flight": flight, + "fromaddr": data.get("signal_unit", {}).get("ges_id"), + "fromaddr_decoded": from_decoded, + "end": True + } + + if data.get("libacars", {}).get("arinc622", {}).get("cpdlc"): + out["decodedText"] = { + "decoder": { + "decodedStatus": "partial" + }, + "formatted": { + "label": data.get("libacars", {}).get("arinc622", {}).get("msg_type", ""), + "value": dumps(data.get("libacars", {}).get("arinc622", {}).get("cpdlc", "")) + } + } + elif data.get("libacars", {}).get("arinc622", {}).get("adsc"): + out["decodedText"] = { + "decoder": { + "decodedStatus": "partial" + }, + "formatted": { + "label": data.get("libacars", {}).get("arinc622", {}).get("msg_type", ""), + "value": dumps(data.get("libacars", {}).get("arinc622", {}).get("adsc", "")) + } + } + elif data.get("libacars", {}).get("arinc622"): + out["decodedText"] = { + "decoder": { + "decodedStatus": "partial" + }, + "formatted": { + "label": data.get("libacars", {}).get("arinc622", {}).get("msg_type", ""), + "value": dumps(data.get("libacars", {}).get("arinc622", "")) + } + } + + #pprint(out) + #print() + + for q in txqs: + q.put(dumps(out)+"\r\n") + except KeyboardInterrupt: + exit() + except BaseException: + print("Other exception:", file=stderr) + pprint(data, stream=stderr) + print(traceback.format_exc(), file=stderr) + pass