diff --git a/csdr/chain/dumpvdl2.py b/csdr/chain/dumpvdl2.py index 4d60fcc3..809a690a 100644 --- a/csdr/chain/dumpvdl2.py +++ b/csdr/chain/dumpvdl2.py @@ -1,6 +1,5 @@ from csdr.chain.demodulator import ServiceDemodulator -from csdr.module import JsonParser -from owrx.vdl2.dumpvdl2 import DumpVDL2Module +from owrx.vdl2.dumpvdl2 import DumpVDL2Module, VDL2MessageParser from pycsdr.modules import Convert from pycsdr.types import Format @@ -10,7 +9,7 @@ class DumpVDL2(ServiceDemodulator): super().__init__([ Convert(Format.COMPLEX_FLOAT, Format.COMPLEX_SHORT), DumpVDL2Module(), - JsonParser("VDL2") + VDL2MessageParser(), ]) def getFixedAudioRate(self) -> int: diff --git a/htdocs/lib/MessagePanel.js b/htdocs/lib/MessagePanel.js index 3ecca55e..25df9b98 100644 --- a/htdocs/lib/MessagePanel.js +++ b/htdocs/lib/MessagePanel.js @@ -469,6 +469,9 @@ AircraftMessagePanel = function(el) { AircraftMessagePanel.prototype = Object.create(MessagePanel.prototype); AircraftMessagePanel.prototype.renderAcars = function(acars) { + if (acars['more']) { + return '

Partial ACARS message

'; + } var details = '

ACARS message

'; if ('flight' in acars) { details += '
Flight: ' + acars['flight'] + '
'; @@ -505,6 +508,7 @@ AircraftMessagePanel.prototype.renderAcars = function(acars) { } } else { // plain text + details += '
Label: ' + acars['label'] + '
'; details += '
' + acars['msg_text'] + '
'; } return details; diff --git a/htdocs/map.js b/htdocs/map.js index 68ef0025..3e213f36 100644 --- a/htdocs/map.js +++ b/htdocs/map.js @@ -404,10 +404,11 @@ $(function(){ }; var linkifyAircraft = function(source, identification) { - var aircraftString = identification || source.icao || source.flight; + var aircraftString = identification || source.humanReadable || source.flight || source.icao; var link = false; switch (aircraft_tracking_service) { case 'flightaware': + if (!source.icao) break; link = 'https://flightaware.com/live/modes/' + source.icao; if (identification) link += "/ident/" + identification link += '/redirect'; diff --git a/owrx/adsb/modes.py b/owrx/adsb/modes.py index 40bcfa5b..9fc37c8b 100644 --- a/owrx/adsb/modes.py +++ b/owrx/adsb/modes.py @@ -1,14 +1,15 @@ from csdr.module import PickleModule from math import sqrt, atan2, pi, floor, acos, cos -from owrx.map import LatLngLocation, IncrementalUpdate, Location, Map, Source +from owrx.map import IncrementalUpdate, Location, Map, Source from owrx.metrics import Metrics, CounterMetric +from owrx.aeronautical import AirplaneLocation, IcaoSource from datetime import datetime, timedelta from enum import Enum FEET_PER_METER = 3.28084 -class AirplaneLocation(IncrementalUpdate, LatLngLocation): +class AdsbLocation(IncrementalUpdate, AirplaneLocation): mapKeys = [ "lat", "lon", @@ -26,12 +27,7 @@ class AirplaneLocation(IncrementalUpdate, LatLngLocation): def __init__(self, message): self.history = [] self.timestamp = datetime.now() - self.props = message - if "lat" in message and "lon" in message: - super().__init__(message["lat"], message["lon"]) - else: - self.lat = None - self.lon = None + super().__init__(message) def update(self, previousLocation: Location): history = previousLocation.history @@ -53,29 +49,11 @@ class AirplaneLocation(IncrementalUpdate, LatLngLocation): if "lon" in merged: self.lon = merged["lon"] - def __dict__(self): - res = super().__dict__() - res.update(self.props) - return res - - -class AdsbLocation(AirplaneLocation): def getTTL(self) -> timedelta: # fixed ttl for adsb-locations for now return timedelta(seconds=30) -class IcaoSource(Source): - def __init__(self, icao: str): - self.icao = icao - - def getKey(self) -> str: - return "icao:{}".format(self.icao) - - def __dict__(self): - return {"icao": self.icao} - - class CprRecordType(Enum): AIR = ("air", 360) GROUND = ("ground", 90) @@ -290,8 +268,8 @@ class ModeSParser(PickleModule): self.metrics.inc() - if "icao" in message and AirplaneLocation.mapKeys & message.keys(): - data = {k: message[k] for k in AirplaneLocation.mapKeys if k in message} + if "icao" in message and AdsbLocation.mapKeys & message.keys(): + data = {k: message[k] for k in AdsbLocation.mapKeys if k in message} loc = AdsbLocation(data) Map.getSharedInstance().updateLocation(IcaoSource(message['icao']), loc, "ADS-B", None) diff --git a/owrx/aeronautical.py b/owrx/aeronautical.py new file mode 100644 index 00000000..a01feb47 --- /dev/null +++ b/owrx/aeronautical.py @@ -0,0 +1,75 @@ +from owrx.map import Map, LatLngLocation, Source +from csdr.module import JsonParser +from abc import ABCMeta + + +class AirplaneLocation(LatLngLocation): + def __init__(self, message): + self.props = message + if "lat" in message and "lon" in message: + super().__init__(message["lat"], message["lon"]) + else: + self.lat = None + self.lon = None + + def __dict__(self): + res = super().__dict__() + res.update(self.props) + return res + + +class IcaoSource(Source): + def __init__(self, icao: str, humanReadable: str = None): + self.icao = icao + self.humanReadable = humanReadable + + def getKey(self) -> str: + return "icao:{}".format(self.icao) + + def __dict__(self): + d = {"icao": self.icao} + if self.humanReadable is not None: + d["humanReadable"] = self.humanReadable + return d + + +class AcarsSource(Source): + def __init__(self, flight): + self.flight = flight + + def getKey(self) -> str: + return "acars:{}".format(self.flight) + + def __dict__(self): + return {"flight": self.flight} + + +class AcarsProcessor(JsonParser, metaclass=ABCMeta): + def processAcars(self, acars: dict, icao: str = None): + if "flight" in acars: + flight_id = acars["flight"] + elif "reg" in acars: + flight_id = acars['reg'] + else: + return + + if "arinc622" in acars: + arinc622 = acars["arinc622"] + if "adsc" in arinc622: + adsc = arinc622["adsc"] + if "tags" in adsc: + for tag in adsc["tags"]: + if "basic_report" in tag: + basic_report = tag["basic_report"] + msg = { + "lat": basic_report["lat"], + "lon": basic_report["lon"], + "altitude": basic_report["alt"] + } + if icao is not None: + source = IcaoSource(icao, humanReadable=flight_id) + else: + source = AcarsSource(flight_id) + Map.getSharedInstance().updateLocation( + source, AirplaneLocation(msg), "ACARS over {}".format(self.mode) + ) diff --git a/owrx/hfdl/dumphfdl.py b/owrx/hfdl/dumphfdl.py index 7a90b10c..47e8ec8c 100644 --- a/owrx/hfdl/dumphfdl.py +++ b/owrx/hfdl/dumphfdl.py @@ -1,7 +1,6 @@ from pycsdr.modules import ExecModule from pycsdr.types import Format -from csdr.module import JsonParser -from owrx.adsb.modes import AirplaneLocation +from owrx.aeronautical import AirplaneLocation, AcarsProcessor, IcaoSource from owrx.map import Map, Source @@ -37,7 +36,7 @@ class DumpHFDLModule(ExecModule): ) -class HFDLMessageParser(JsonParser): +class HFDLMessageParser(AcarsProcessor): def __init__(self): super().__init__("HFDL") @@ -47,11 +46,34 @@ class HFDLMessageParser(JsonParser): payload = msg["hfdl"] if "lpdu" in payload: lpdu = payload["lpdu"] + icao = lpdu["src"]["ac_info"]["icao"] if "ac_info" in lpdu["src"] else None if lpdu["type"]["id"] in [13, 29]: hfnpdu = lpdu["hfnpdu"] if hfnpdu["type"]["id"] == 209: - if "pos" in hfnpdu: - pos = hfnpdu['pos'] - if abs(pos['lat']) <= 90 and abs(pos['lon']) <= 180: - Map.getSharedInstance().updateLocation(HfdlSource(hfnpdu["flight_id"]), HfdlAirplaneLocation(pos), "HFDL") + # performance data + self.processPosition(hfnpdu, icao) + elif hfnpdu["type"]["id"] == 255: + # enveloped data + if "acars" in hfnpdu: + self.processAcars(hfnpdu["acars"], icao) + elif lpdu["type"]["id"] in [79, 143, 191]: + if "ac_info" in lpdu: + icao = lpdu["ac_info"]["icao"] + self.processPosition(lpdu["hfnpdu"], icao) + return msg + + def processPosition(self, hfnpdu, icao=None): + if "pos" in hfnpdu: + pos = hfnpdu["pos"] + if abs(pos['lat']) <= 90 and abs(pos['lon']) <= 180: + msg = { + "lat": pos["lat"], + "lon": pos["lon"], + "flight": hfnpdu["flight_id"] + } + if icao is None: + source = HfdlSource(hfnpdu["flight_id"]) + else: + source = IcaoSource(icao, humanReadable=hfnpdu["flight_id"]) + Map.getSharedInstance().updateLocation(source, HfdlAirplaneLocation(msg), "HFDL") diff --git a/owrx/vdl2/dumpvdl2.py b/owrx/vdl2/dumpvdl2.py index 9f08dc1f..48af2357 100644 --- a/owrx/vdl2/dumpvdl2.py +++ b/owrx/vdl2/dumpvdl2.py @@ -1,5 +1,8 @@ from pycsdr.modules import ExecModule from pycsdr.types import Format +from owrx.aeronautical import AcarsProcessor +from owrx.map import Map +from owrx.aeronautical import AirplaneLocation, IcaoSource class DumpVDL2Module(ExecModule): @@ -15,3 +18,68 @@ class DumpVDL2Module(ExecModule): "--output", "decoded:json:file:path=-", ] ) + + +class VDL2MessageParser(AcarsProcessor): + def __init__(self): + super().__init__("VDL2") + + def process(self, line): + msg = super().process(line) + if msg is not None: + payload = msg["vdl2"] + if "avlc" in payload: + avlc = payload["avlc"] + src = avlc["src"]["addr"] + if avlc["frame_type"] == "I": + if "acars" in avlc: + self.processAcars(avlc["acars"]) + elif "x25" in avlc: + x25 = avlc["x25"] + if "clnp" in x25: + clnp = x25["clnp"] + if "cotp" in clnp: + cotp = clnp["cotp"] + if "adsc_v2" in cotp: + adsc_v2 = cotp["adsc_v2"] + if "adsc_report" in adsc_v2: + adsc_report = adsc_v2["adsc_report"] + if "periodic_report" in adsc_report["data"]: + periodic_report = adsc_report["data"]["periodic_report"] + report_data = periodic_report["report_data"] + self.processReport(report_data, src) + return msg + + def processReport(self, report, icao): + if "position" not in report: + return + msg = { + "lat": self.convertLatitude(**report["position"]["lat"]), + "lon": self.convertLongitude(**report["position"]["lon"]), + "altitude": report["position"]["alt"]["val"], + } + if "ground_vector" in report: + msg.update({ + "groundtrack": report["ground_vector"]["ground_track"]["val"], + "groundspeed": report["ground_vector"]["ground_speed"]["val"], + }) + if "air_vector" in report: + msg.update({ + "verticalspeed": report["air_vector"]["vertical_rate"]["val"], + }) + Map.getSharedInstance().updateLocation(IcaoSource(icao), AirplaneLocation(msg), "VDL2") + + def convertLatitude(self, dir, **args) -> float: + coord = self.convertCoordinate(**args) + if dir == "south": + coord *= -1 + return coord + + def convertLongitude(self, dir, **args) -> float: + coord = self.convertCoordinate(**args) + if dir == "west": + coord *= -1 + return coord + + def convertCoordinate(self, deg, min, sec) -> float: + return deg + float(min) / 60 + float(sec) / 3600