diff --git a/csdr/chain/toolbox.py b/csdr/chain/toolbox.py
index 5acc7628..b7125928 100644
--- a/csdr/chain/toolbox.py
+++ b/csdr/chain/toolbox.py
@@ -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
diff --git a/csdr/module/toolbox.py b/csdr/module/toolbox.py
index cb441be4..a8be115b 100644
--- a/csdr/module/toolbox.py
+++ b/csdr/module/toolbox.py
@@ -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)
diff --git a/htdocs/lib/MapMarkers.js b/htdocs/lib/MapMarkers.js
index 453d0cb8..b600aeab 100644
--- a/htdocs/lib/MapMarkers.js
+++ b/htdocs/lib/MapMarkers.js
@@ -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 = '
' + Marker.makeListTitle('Details') + detailsString + '
';
}
diff --git a/owrx/aircraft.py b/owrx/aircraft.py
index 3964b5ce..30583f61 100644
--- a/owrx/aircraft.py
+++ b/owrx/aircraft.py
@@ -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
diff --git a/owrx/service/__init__.py b/owrx/service/__init__.py
index 9197efbe..938e57dc 100644
--- a/owrx/service/__init__.py
+++ b/owrx/service/__init__.py
@@ -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))