Updated to the latest Mode-S decoder from Jakob, updated message types.

This commit is contained in:
Marat Fayzullin 2023-08-31 20:23:52 -04:00
parent 477161f884
commit bd01c210ae
2 changed files with 162 additions and 54 deletions

View File

@ -1,27 +1,17 @@
from abc import ABC
from csdr.module import PickleModule
from math import sqrt, atan2, pi, floor, acos, cos
#from owrx.map import LatLngLocation, IncrementalUpdate, Location, Map
#from owrx.map import LatLngLocation, IncrementalUpdate, TTLUpdate, Location, Map
from owrx.map import LatLngLocation, Location, Map
from owrx.metrics import Metrics, CounterMetric
from datetime import timedelta
from enum import Enum
import time
import logging
logger = logging.getLogger(__name__)
FEET_PER_METER = 3.28084
nz = 15
d_lat_even = 360 / (4 * nz)
d_lat_odd = 360 / (4 * nz - 1)
#class AirplaneLocation(LatLngLocation, IncrementalUpdate, ABC):
class AirplaneLocation(LatLngLocation, ABC):
class AirplaneLocation(LatLngLocation):
mapKeys = [
"icao",
"lat",
"lon",
"altitude",
@ -30,13 +20,17 @@ class AirplaneLocation(LatLngLocation, ABC):
"groundspeed",
"verticalspeed",
"identification",
"TAS",
"IAS",
"heading",
]
ttl = 30
def __init__(self, message):
def __init__(self, icao, message):
self.history = []
self.timestamp = time.time()
self.props = message
self.icao = icao
if "lat" in message and "lon" in message:
super().__init__(message["lat"], message["lon"])
else:
@ -45,13 +39,13 @@ class AirplaneLocation(LatLngLocation, ABC):
def update(self, previousLocation: Location):
history = previousLocation.history
now = time.time()
history = [p for p in history if now - p["timestamp"] < self.ttl]
history += [{
"timestamp": self.timestamp,
"props": self.props,
}]
now = time.time()
history = [p for p in history if now - p["timestamp"] < self.ttl]
self.history = sorted(history, key=lambda p: p["timestamp"])
self.history = history
merged = {}
for p in self.history:
@ -66,31 +60,61 @@ class AirplaneLocation(LatLngLocation, ABC):
def __dict__(self):
dict = super().__dict__()
dict.update(self.props)
dict["icao"] = self.icao
return dict
def getTTL(self) -> timedelta:
return timedelta(seconds=self.ttl)
class CprRecordType(Enum):
AIR = ("air", 360)
GROUND = ("ground", 90)
def __new__(cls, *args, **kwargs):
name, baseAngle = args
obj = object.__new__(cls)
obj._value_ = name
obj.baseAngle = baseAngle
return obj
class CprCache:
def __init__(self):
self.aircraft = {}
self.airRecords = {}
self.groundRecords = {}
def getRecentData(self, icao: str):
if icao not in self.aircraft:
def __getRecords(self, cprType: CprRecordType):
if cprType is CprRecordType.AIR:
return self.airRecords
elif cprType is CprRecordType.GROUND:
return self.groundRecords
def getRecentData(self, icao: str, cprType: CprRecordType):
records = self.__getRecords(cprType)
if icao not in records:
return []
now = time.time()
filtered = [r for r in self.aircraft[icao] if now - r["timestamp"] < 10]
records = sorted(filtered, key=lambda r: r["timestamp"])
self.aircraft[icao] = records
return [r["data"] for r in records]
filtered = [r for r in records[icao] if now - r["timestamp"] < 10]
records_sorted = sorted(filtered, key=lambda r: r["timestamp"])
records[icao] = records_sorted
return [r["data"] for r in records_sorted]
def addRecord(self, icao: str, data: any):
if icao not in self.aircraft:
self.aircraft[icao] = []
self.aircraft[icao].append({"timestamp": time.time(), "data": data})
def addRecord(self, icao: str, data: any, cprType: CprRecordType):
records = self.__getRecords(cprType)
if icao not in records:
records[icao] = []
records[icao].append({"timestamp": time.time(), "data": data})
class ModeSParser(PickleModule):
def __init__(self):
self.cprCache = CprCache()
name = "dump1090.decodes.adsb"
self.metrics = Metrics.getSharedInstance().getMetric(name)
if self.metrics is None:
self.metrics = CounterMetric()
Metrics.getSharedInstance().addMetric(name, self.metrics)
super().__init__()
def process(self, input):
@ -118,16 +142,43 @@ class ModeSParser(PickleModule):
input[10] & 0b00111111
]
message["identification"] = bytes(b + (0x40 if b < 27 else 0) for b in id).decode("ascii")
message["identification"] = bytes(b + (0x40 if b < 27 else 0) for b in id).decode("ascii").strip()
elif type in [5, 6, 7, 8]:
# surface position
pass
# there's no altitude data in this message type, but the type implies the aircraft is on ground
message["altitude"] = "ground"
movement = ((input[4] & 0b00000111) << 4) | ((input[5] & 0b11110000) >> 4)
if movement == 1:
message["groundspeed"] = 0
elif 2 <= movement < 9:
message["groundspeed"] = (movement - 1) * .0125
elif 9 <= movement < 13:
message["groundspeed"] = 1 + (movement - 8) * .25
elif 13 <= movement < 39:
message["groundspeed"] = 2 + (movement - 12) * .5
elif 39 <= movement < 94:
message["groundspeed"] = 15 + (movement - 38) # * 1
elif 94 <= movement < 109:
message["groundspeed"] = 70 + (movement - 108) * 2
elif 109 <= movement < 124:
message["groundspeeed"] = 100 + (movement - 123) * 5
if (input[5] & 0b00001000) >> 3:
track = ((input[5] & 0b00000111) << 3) | ((input[6] & 0b11110000) >> 4)
message["groundtrack"] = (360 * track) / 128
cpr = self.__getCprData(icao, input, CprRecordType.GROUND)
if cpr is not None:
lat, lon = cpr
message["lat"] = lat
message["lon"] = lon
elif type in [9, 10, 11, 12, 13, 14, 15, 16, 17, 18]:
# airborne position (w/ baro altitude)
cpr = self.__getCprData(icao, input)
cpr = self.__getCprData(icao, input, CprRecordType.AIR)
if cpr is not None:
lat, lon = cpr
message["lat"] = lat
@ -137,9 +188,10 @@ class ModeSParser(PickleModule):
altitude = ((input[5] & 0b11111110) << 3) | ((input[6] & 0b11110000) >> 4)
if q:
message["altitude"] = altitude * 25 - 1000
else:
# TODO: it's gray encoded
message["altitude"] = altitude * 100
elif altitude > 0:
altitude = self._gillhamDecode(altitude)
if altitude is not None:
message["altitude"] = altitude
elif type == 19:
# airborne velocity
@ -187,7 +239,6 @@ class ModeSParser(PickleModule):
if sh:
hdg = ((input[5] & 0b00000011) << 8) | input[6]
message["heading"] = hdg * 360 / 1024
logger.debug("decoded from subtype 3: heading = %i", message["heading"])
airspeed = ((input[7] & 0b01111111) << 3) | ((input[8] & 0b11100000) >> 5)
if airspeed != 0:
airspeed -= 1
@ -197,15 +248,13 @@ class ModeSParser(PickleModule):
airspeed_type = (input[7] & 0b10000000) >> 7
if airspeed_type:
message["TAS"] = airspeed
logger.debug("decoded from subtype 3: TAS = %i", message["TAS"])
else:
message["IAS"] = airspeed
logger.debug("decoded from subtype 3: IAS = %i", message["IAS"])
elif type in [20, 21, 22]:
# airborne position (w/GNSS height)
cpr = self.__getCprData(icao, input)
cpr = self.__getCprData(icao, input, CprRecordType.AIR)
if cpr is not None:
lat, lon = cpr
message["lat"] = lat
@ -230,21 +279,23 @@ class ModeSParser(PickleModule):
# Mode-S All-call reply
message["icao"] = input[1:4].hex()
#if "icao" in message and AirplaneLocation.mapKeys & message.keys():
# data = {k: message[k] for k in AirplaneLocation.mapKeys if k in message}
# loc = AirplaneLocation(data)
# Map.getSharedInstance().updateLocation({"icao": message['icao']}, loc, "ADS-B", None)
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}
# loc = AirplaneLocation(message["icao"], data)
# Map.getSharedInstance().updateLocation({"icao": message['icao']}, loc, "ADS-B", None)
return message
def __getCprData(self, icao: str, input):
def __getCprData(self, icao: str, input, cprType: CprRecordType):
self.cprCache.addRecord(icao, {
"cpr_format": (input[6] & 0b00000100) >> 2,
"lat_cpr": ((input[6] & 0b00000011) << 15) | (input[7] << 7) | ((input[8] & 0b11111110) >> 1),
"lon_cpr": ((input[8] & 0b00000001) << 16) | (input[9] << 8) | (input[10]),
})
}, cprType)
records = self.cprCache.getRecentData(icao)
records = self.cprCache.getRecentData(icao, cprType)
try:
# records are sorted by timestamp, last should be newest
@ -258,6 +309,10 @@ class ModeSParser(PickleModule):
# latitude zone index
j = floor(59 * lat_cpr_even - 60 * lat_cpr_odd + .5)
nz = 15
d_lat_even = cprType.baseAngle / (4 * nz)
d_lat_odd = cprType.baseAngle / (4 * nz - 1)
lat_even = d_lat_even * ((j % 60) + lat_cpr_even)
lat_odd = d_lat_odd * ((j % 59) + lat_cpr_odd)
@ -296,8 +351,8 @@ class ModeSParser(PickleModule):
n_even = max(nl_lat, 1)
n_odd = max(nl_lat - 1, 1)
d_lon_even = 360 / n_even
d_lon_odd = 360 / n_odd
d_lon_even = cprType.baseAngle / n_even
d_lon_odd = cprType.baseAngle / n_odd
lon_even = d_lon_even * (m % n_even + lon_cpr_even)
lon_odd = d_lon_odd * (m % n_odd + lon_cpr_odd)
@ -311,3 +366,36 @@ class ModeSParser(PickleModule):
except StopIteration:
# we don't have both CPR records. better luck next time.
pass
def _grayDecode(self, input: int):
l = input.bit_length()
previous_bit = 0
output = 0
for i in reversed(range(0, l)):
bit = (previous_bit ^ ((input >> i) & 1))
output |= bit << i
previous_bit = bit
return output
gianniTable = [None, -200, 0, -100, 200, None, 100, None]
def _gillhamDecode(self, input: int):
c = ((input & 0b10000000000) >> 8) | ((input & 0b00100000000) >> 7) | ((input & 0b00001000000) >> 6)
b = ((input & 0b00000010000) >> 2) | ((input & 0b00000001000) >> 2) | ((input & 0b00000000010) >> 1)
a = ((input & 0b01000000000) >> 7) | ((input & 0b00010000000) >> 6) | ((input & 0b00000100000) >> 5)
d = ((input & 0b00000000100) >> 1) | (input & 0b00000000001)
dab = (d << 6) | (a << 3) | b
parity = dab.bit_count() % 2
offset = self.gianniTable[c]
if offset is None:
# invalid decode...
return None
if parity:
offset *= -1
altitude = self._grayDecode(dab) * 500 + offset - 1000
return altitude

View File

@ -15,9 +15,22 @@ logger = logging.getLogger(__name__)
#
# Feet per meter
#
FEET_PER_METER = 3.28084
METERS_PER_FOOT = 0.3048
KMH_PER_KNOT = 1.852
#
# Mode-S message formats
#
MODE_S_FORMATS = [
"Short ACAS", None, None, None,
"Altitude", "IDENT Reply", None, None,
None, None, None, "ADSB",
None, None, None, None,
"Long ACAS", "Extended ADSB", "Supplementary ADSB", "Exetended Military",
"Comm-B Altitude", "Comm-B IDENT Reply", "Military", None,
"Comm-D Message"
]
#
# This class represents current aircraft location compatible with
# the APRS markers. It can be used for displaying aircraft on the
@ -347,7 +360,7 @@ class Vdl2Parser(AircraftParser):
out["lat"] = p["value"]["loc"]["lat"]
out["lon"] = p["value"]["loc"]["lon"]
# Convert altitude from feet into meters
out["altitude"] = round(p["value"]["alt"] / FEET_PER_METER)
out["altitude"] = round(p["value"]["alt"] * METERS_PER_FOOT)
elif p["name"] == "dst_airport":
# Parse destination airport
out["airport"] = p["value"]
@ -379,16 +392,23 @@ class AdsbParser(AircraftParser):
out["ts"] = now.timestamp()
out["time"] = now.strftime("%H:%M:%S")
out["icao"] = out["icao"].upper()
out["type"] = "ADSB Format {0} frame".format(out["format"])
# Determine message format and type
format = out["format"]
if format >= len(MODE_S_FORMATS) or not MODE_S_FORMATS[format]:
out["type"] = "Mode-S Format {0} frame".format(format)
elif format == 17:
out["type"] = "ADSB Type {0} frame".format(out["adsb_type"])
else:
out["type"] = "Mode-S {0} frame".format(MODE_S_FORMATS[format])
# Flight ID, if present
if "identification" in out:
out["flight"] = out["identification"].strip()
# Altitude, if present
if "altitude" in out:
out["altitude"] = round(out["altitude"] / FEET_PER_METER)
out["altitude"] = round(out["altitude"] * METERS_PER_FOOT)
# Vertical speed, if present
if "verticalspeed" in out:
out["vspeed"] = round(out["verticalspeed"] / FEET_PER_METER)
out["vspeed"] = round(out["verticalspeed"] * METERS_PER_FOOT)
# Speed, if present
if "groundspeed" in out:
out["speed"] = round(out["groundspeed"] * KMH_PER_KNOT)