Adding optional ADSB parsing of the dump1090 JSON output.

This commit is contained in:
Marat Fayzullin 2023-09-22 14:38:16 -04:00
parent 7e31abc4ec
commit 0f41604b65
5 changed files with 160 additions and 42 deletions

View File

@ -5,6 +5,7 @@ from pycsdr.types import Format
from owrx.toolbox import TextParser, PageParser, SelCallParser, IsmParser
from owrx.aircraft import HfdlParser, Vdl2Parser, AdsbParser, AcarsParser
import os
class IsmDemodulator(ServiceDemodulator, DialFrequencyReceiver):
def __init__(self, service: bool = False):
@ -145,12 +146,13 @@ class Vdl2Demodulator(ServiceDemodulator, DialFrequencyReceiver):
class AdsbDemodulator(ServiceDemodulator, DialFrequencyReceiver):
def __init__(self, service: bool = False):
def __init__(self, service: bool = False, jsonFile: str = None):
self.sampleRate = 2400000
self.parser = AdsbParser(service=service)
self.parser = AdsbParser(service=service, jsonFile=jsonFile)
jsonFolder = os.path.dirname(jsonFile) if jsonFile else None
workers = [
Convert(Format.COMPLEX_FLOAT, Format.COMPLEX_SHORT),
Dump1090Module(rawOutput = True, jsonOutput = True),
Dump1090Module(rawOutput = True, jsonFolder = jsonFolder),
self.parser,
]
# Connect all the workers

View File

@ -77,8 +77,8 @@ class DumpVdl2Module(PopenModule):
class Dump1090Module(ExecModule):
def __init__(self, rawOutput: bool = False, jsonOutput: bool = False):
self.jsonFolder = "/tmp/dump1090"
def __init__(self, rawOutput: bool = False, jsonFolder: str = None):
self.jsonFolder = jsonFolder
pm = Config.get()
lat = pm["receiver_gps"]["lat"]
lon = pm["receiver_gps"]["lon"]
@ -87,13 +87,16 @@ class Dump1090Module(ExecModule):
"--lat", str(lat), "--lon", str(lon),
"--modeac", "--metric"
]
if jsonOutput:
# If JSON files folder supplied, use that, disable STDOUT output
if self.jsonFolder is not None:
try:
os.makedirs(self.jsonFolder, exist_ok = True)
cmd += [ "--write-json", self.jsonFolder ]
cmd += [ "--quiet", "--write-json", self.jsonFolder ]
except:
self.jsonFolder = None
pass
if rawOutput:
# RAW STDOUT output only makes sense if we are not using JSON
if rawOutput and self.jsonFolder is None:
cmd += [ "--raw" ]
super().__init__(Format.COMPLEX_SHORT, Format.CHAR, cmd)

View File

@ -421,6 +421,8 @@ AprsMarker.prototype.update = function(update) {
this.flight = update.location.flight;
this.icao = update.location.icao;
this.vspeed = update.location.vspeed;
this.squawk = update.location.squawk;
this.rssi = update.location.rssi;
this.msglog = update.location.msglog;
// Implementation-dependent function call
@ -608,6 +610,10 @@ AprsMarker.prototype.getInfoHTML = function(name, receiverMarker = null) {
detailsString += Marker.makeListItem('Aircraft', Marker.linkify(this.aircraft, flight_url));
}
if (this.squawk) {
detailsString += Marker.makeListItem('Squawk', this.squawk);
}
if (this.origin) {
detailsString += Marker.makeListItem('Origin', this.origin);
}
@ -639,6 +645,10 @@ AprsMarker.prototype.getInfoHTML = function(name, receiverMarker = null) {
detailsString += Marker.makeListItem('Altitude', alt);
}
if (this.rssi) {
detailsString += Marker.makeListItem('RSSI', this.rssi + ' dB');
}
if (detailsString.length > 0) {
detailsString = '<div>' + Marker.makeListTitle('Details') + detailsString + '</div>';
}

View File

@ -10,6 +10,7 @@ import json
import math
import time
import re
import os
import logging
@ -34,6 +35,37 @@ MODE_S_FORMATS = [
"Comm-D Message"
]
#
# Aircraft categories
#
ADSB_CATEGORIES = {
"A0": ("^", "/"), # No ADS-B emitter category information
"A1": ("'", "/"), # Light (< 15500 lbs)
"A2": ("'", "/"), # Small (15500 to 75000 lbs)
"A3": ("^", "/"), # Large (75000 to 300000 lbs)
"A4": ("^", "/"), # High vortex large (aircraft such as B-757)
"A5": ("^", "/"), # Heavy (> 300000 lbs)
"A6": ("^", "/"), # High performance (> 5g acceleration and 400 kts)
"A7": ("X", "/"), # Rotorcraft, regardless of weight
"B0": ("^", "/"), # No ADS-B emitter category information
"B1": ("g", "/"), # Glider or sailplane, regardless of weight
"B2": ("O", "/"), # Airship or balloon, regardless of weight
"B3": ("g", "/"), # Parachutist / skydiver
"B4": ("g", "/"), # Ultralight / hang-glider / paraglider
"B5": ("^", "/"), # Reserved
"B6": ("S", "\\"), # Unmanned aerial vehicle, regardless of weight
"B7": ("S", "/"), # Space / trans-atmospheric vehicle
"C0": ("D", "/"), # No ADS-B emitter category information
"C1": ("f", "/"), # Surface vehicle emergency vehicle
"C2": ("u", "\\"), # Surface vehicle service vehicle
"C3": ("D", "/"), # Point obstacle (includes tethered balloons)
"C4": ("D", "/"), # Cluster obstacle
"C5": ("D", "/"), # Line obstacle
"C6": ("D", "/"), # Reserved
"C7": ("D", "/"), # Reserved
}
#
# This class represents current aircraft location compatible with
# the APRS markers. It can be used for displaying aircraft on the
@ -47,11 +79,17 @@ class AircraftLocation(LatLngLocation):
def __dict__(self):
res = super(AircraftLocation, self).__dict__()
# Add APRS-like aircraft symbol (red or blue, depending on mode)
mod = '/' if self.data["mode"]=="ADSB" else '\\'
res["symbol"] = getSymbolData('^', mod)
# Add an APRS-like symbol
if "category" in self.data and self.data["category"] in ADSB_CATEGORIES:
# Add APRS-like symbol by aircraft category
cat = ADSB_CATEGORIES[self.data["category"]]
res["symbol"] = getSymbolData(cat[0], cat[1])
else:
# Add APRS-like aircraft symbol (red or blue, depending on mode)
mod = '/' if self.data["mode"]=="ADSB" else '\\'
res["symbol"] = getSymbolData('^', mod)
# Convert aircraft-specific data into APRS-like data
for x in ["icao", "aircraft", "flight", "speed", "altitude", "course", "destination", "origin", "vspeed", "msglog"]:
for x in ["icao", "aircraft", "flight", "speed", "altitude", "course", "destination", "origin", "vspeed", "squawk", "rssi", "msglog"]:
if x in self.data:
res[x] = self.data[x]
# Return APRS-like dictionary object
@ -66,11 +104,6 @@ class AircraftManager(object):
sharedInstance = None
creationLock = threading.Lock()
# Called on a timer
@staticmethod
def _periodicCleanup(arg):
arg.periodicCleanup()
# Return a global instance of the aircraft manager.
@staticmethod
def getSharedInstance():
@ -103,16 +136,19 @@ class AircraftManager(object):
def __init__(self):
self.lock = threading.Lock()
self.checkTime = 60
self.cleanupPeriod = 60
self.maxMsgLog = 20
self.colors = ColorCache()
self.aircraft = {}
self.periodicCleanup()
# Start periodic cleanup task
self.thread = threading.Thread(target=self._cleanupThread)
self.thread.start()
# Perform periodic cleanup
def periodicCleanup(self):
self.cleanup()
threading.Timer(self.checkTime, self._periodicCleanup, [self]).start()
def _cleanupThread(self):
while self.thread is not None:
time.sleep(self.cleanupPeriod)
self.cleanup()
# Get aircraft data by ID.
def getAircraft(self, id):
@ -453,17 +489,30 @@ class Vdl2Parser(AircraftParser):
# Parser for ADSB messages coming from Dump1090 in hexadecimal format.
#
class AdsbParser(AircraftParser):
def __init__(self, service: bool = False):
def __init__(self, service: bool = False, jsonFile: str = None):
super().__init__(filePrefix=None, service=service)
self.smode_parser = ModeSParser()
self.jsonFile = jsonFile
self.checkPeriod = 1
self.lastParse = 0
# Start periodic JSON file check
if self.jsonFile is not None:
self.thread = threading.Thread(target=self._refreshThread)
self.thread.start()
# Default parsing function called by AircraftParser class
def parseAircraft(self, msg: bytes):
# When using Dump1090 JSON file, do not parse here
if self.jsonFile is not None:
return None
# If it is a valid Mode-S message...
if msg.startswith(b"*") and msg.endswith(b";") and len(msg) in [16, 30]:
# Parse Mode-S message
msg = msg[1:-1].decode('utf-8', 'replace')
out = self.smode_parser.process(bytes.fromhex(msg))
#logger.debug("@@@ PARSE OUT: {0}".format(out))
# Only consider position and identification reports for now
if "identification" in out or "groundspeed" in out or ("lat" in out and "lon" in out):
# Add fields for compatibility with other aircraft parsers
@ -508,7 +557,24 @@ class AdsbParser(AircraftParser):
# No data parsed
return None
def parseDump1090Json(self, file: str, updateDatabase: bool = False):
# Periodically check if Dump1090's JSON file has changed
# and parse it if it has.
def _refreshThread(self):
lastUpdate = 0
while self.thread is not None:
# Wait until the next check
time.sleep(self.checkPeriod)
try:
# If JSON file has updated since the last update, parse it
ts = os.path.getmtime(self.jsonFile)
if ts > lastUpdate:
self.parseJson(self.jsonFile, updateDatabase=True)
lastUpdate = ts
except Exception as exptn:
logger.info("Failed to check file '{0}': {1}".format(self.jsonFile, exptn))
# Parse supplied JSON file in Dump1090 format.
def parseJson(self, file: str, updateDatabase: bool = False):
# Load JSON from supplied file
try:
with open(file, "r") as f:
@ -519,56 +585,92 @@ class AdsbParser(AircraftParser):
# Make sure we have the aircraft data
if "aircraft" not in data or "now" not in data:
return None
elif not updateDatabase:
# Return original JSON "aircraft" contents
return data["aircraft"]
# The Dump1090 JSON data has a special mode
data["mode"] = "ADSB-LIST"
# If not updating aircraft database, exit here
if not updateDatabase:
return data
# Going to add timestamps and TTLs
pm = Config.get()
ts = data["now"]
ttl = ts + pm["adsb_ttl"]
data = data["aircraft"]
now = data["now"]
ttl = now + pm["adsb_ttl"]
# Iterate over aircraft
for entry in data:
for entry in data["aircraft"]:
# Do not update twice
ts = now - entry["seen"]
if ts <= self.lastParse:
continue
# Always present ADSB data
out = {
"mode" : "ADSB",
"icao" : entry["hex"].upper(),
"ts" : ts,
"ttl" : ttl,
"ttl" : ttl - entry["seen"],
"msgs" : entry["messages"],
"seen" : entry["seen"],
"rssi" : entry["rssi"],
}
# Position
if "lat" in entry and "lon" in entry:
out["lat"] = entry["lat"]
out["lon"] = entry["lon"]
# Flight identification, aircraft type, squawk code
if "flight" in entry:
out["flight"] = entry["flight"].strip()
if "category" in entry:
out["category"] = entry["category"]
if "squawk" in entry:
out["type"] = "Squawk " + entry["squawk"]
elif "category" in entry:
out["type"] = "Category " + entry["category"]
out["squawk"] = entry["squawk"]
if "emergency" in entry and entry["emergency"] != "none":
out["emergency"] = entry["emergency"].upper()
# Altitude
if "alt_geom" in entry:
out["altitude"] = round(entry["alt_geom"] * METERS_PER_FOOT)
elif "alt_baro" in entry:
out["altitude"] = round(entry["alt_baro"] * METERS_PER_FOOT)
elif "nav_altitude_mcp" in entry:
out["altitude"] = round(entry["nav_altitude_mcp"] * METERS_PER_FOOT)
if "nav_heading" in entry:
out["course"] = round(entry["nav_heading"])
# Climb/descent rate
if "geom_rate" in entry:
out["vspeed"] = round(entry["geom_rate"] * METERS_PER_FOOT)
elif "baro_rate" in entry:
out["vspeed"] = round(entry["baro_rate"] * METERS_PER_FOOT)
# Speed
if "gs" in entry:
out["speed"] = round(entry["gs"] * KMH_PER_KNOT)
elif "tas" in entry:
out["speed"] = round(entry["tas"] * KMH_PER_KNOT)
elif "ias" in entry:
out["speed"] = round(entry["ias"] * KMH_PER_KNOT)
# Heading
if "true_heading" in entry:
out["course"] = round(entry["true_heading"])
elif "mag_heading" in entry:
out["course"] = round(entry["mag_heading"])
elif "track" in entry:
out["course"] = round(entry["track"])
# Outside temperature
if "oat" in entry:
out["temperature"] = entry["oat"]
elif "tat" in entry:
out["temperature"] = entry["tat"]
# Update aircraft database with the new data
AircraftManager.getSharedInstance().update(out)
# Return original JSON "aircraft" contents
# Save last parsed time
self.lastParse = now
# Return original JSON contents
return data

View File

@ -342,6 +342,7 @@ class ServiceHandler(SdrSourceEventClient):
elif mod == "adsb":
from csdr.chain.toolbox import AdsbDemodulator
return AdsbDemodulator(service=True)
# return AdsbDemodulator(service=True, jsonFile="/tmp/dump1090/aircraft.json")
raise ValueError("unsupported service modulation: {}".format(mod))