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