mirror of
https://github.com/jketterl/openwebrx.git
synced 2026-01-11 03:00:10 +01:00
add lat / lon decoding
This commit is contained in:
parent
36161a2b4b
commit
e37e2db017
|
|
@ -291,6 +291,9 @@ AdsbMessagePanel.prototype.render = function() {
|
|||
'<th class="altitude">Altitude</th>' +
|
||||
'<th class="speed">Speed</th>' +
|
||||
'<th class="track">Track</th>' +
|
||||
'<th class="verticalspeed">V/S</th>' +
|
||||
'<th class="lat">Lat</th>' +
|
||||
'<th class="lon">Long</th>' +
|
||||
'<th class="messages">Messages</th>' +
|
||||
'</tr></thead>' +
|
||||
'<tbody></tbody>' +
|
||||
|
|
@ -322,12 +325,19 @@ AdsbMessagePanel.prototype.pushMessage = function(message) {
|
|||
return "";
|
||||
}
|
||||
|
||||
var coordRound = function(i) {
|
||||
return Math.round(i * 1000) / 1000;
|
||||
}
|
||||
|
||||
state.el.html(
|
||||
'<td>' + state.icao + '</td>' +
|
||||
'<td>' + ifDefined(state.identification) + '</td>' +
|
||||
'<td>' + ifDefined(state.altitude) + '</td>' +
|
||||
'<td>' + ifDefined(state.groundspeed, Math.round) + '</td>' +
|
||||
'<td>' + ifDefined(state.groundtrack, Math.round) + '</td>' +
|
||||
'<td>' + ifDefined(state.groundspeed || state.IAS || state.TAS, Math.round) + '</td>' +
|
||||
'<td>' + ifDefined(state.groundtrack || state.heading, Math.round) + '</td>' +
|
||||
'<td>' + ifDefined(state.verticalspeed) + '</td>' +
|
||||
'<td>' + ifDefined(state.lat, coordRound) + '</td>' +
|
||||
'<td>' + ifDefined(state.lon, coordRound) + '</td>' +
|
||||
'<td>' + state.messages + '</td>'
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
from csdr.module import PickleModule
|
||||
from math import sqrt, atan2, pi
|
||||
from math import sqrt, atan2, pi, floor, acos, cos
|
||||
import time
|
||||
|
||||
import logging
|
||||
|
||||
|
|
@ -8,8 +9,35 @@ 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 CprCache:
|
||||
def __init__(self):
|
||||
self.aircraft = {}
|
||||
|
||||
def getRecentData(self, icao: str):
|
||||
if icao not in self.aircraft:
|
||||
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]
|
||||
|
||||
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})
|
||||
|
||||
|
||||
class ModeSParser(PickleModule):
|
||||
def __init__(self):
|
||||
self.cprCache = CprCache()
|
||||
super().__init__()
|
||||
|
||||
def process(self, input):
|
||||
format = (input[0] & 0b11111000) >> 3
|
||||
message = {
|
||||
|
|
@ -18,11 +46,11 @@ class ModeSParser(PickleModule):
|
|||
}
|
||||
if format == 17:
|
||||
message["capability"] = input[0] & 0b111
|
||||
message["icao"] = input[1:4].hex()
|
||||
message["icao"] = icao = input[1:4].hex()
|
||||
type = (input[4] & 0b11111000) >> 3
|
||||
message["type"] = type
|
||||
|
||||
if type in range(1, 5):
|
||||
if type in [1, 2, 3, 4]:
|
||||
# identification message
|
||||
id = [
|
||||
(input[5] & 0b11111100) >> 2,
|
||||
|
|
@ -37,12 +65,19 @@ class ModeSParser(PickleModule):
|
|||
|
||||
message["identification"] = bytes(b + (0x40 if b < 27 else 0) for b in id).decode("ascii")
|
||||
|
||||
elif type in range(5, 9):
|
||||
elif type in [5, 6, 7, 8]:
|
||||
# surface position
|
||||
pass
|
||||
|
||||
elif type in range(9, 19):
|
||||
elif type in [9, 10, 11, 12, 13, 14, 15, 16, 17, 18]:
|
||||
# airborne position (w/ baro altitude)
|
||||
|
||||
cpr = self.__getCprData(icao, input)
|
||||
if cpr is not None:
|
||||
lat, lon = cpr
|
||||
message["lat"] = lat
|
||||
message["lon"] = lon
|
||||
|
||||
q = (input[5] & 0b1)
|
||||
altitude = ((input[5] & 0b11111110) << 3) | ((input[6] & 0b11110000) >> 4)
|
||||
if q:
|
||||
|
|
@ -54,28 +89,73 @@ class ModeSParser(PickleModule):
|
|||
elif type == 19:
|
||||
# airborne velocity
|
||||
subtype = input[4] & 0b111
|
||||
if subtype in range(1, 3):
|
||||
dew = (input[5] & 0b00000100) >> 2
|
||||
if subtype in [1, 2]:
|
||||
# velocity is reported in an east/west and a north/south component
|
||||
# vew = velocity east / west
|
||||
vew = ((input[5] & 0b00000011) << 8) | input[6]
|
||||
dns = (input[7] & 0b10000000) >> 7
|
||||
# vns = velocity north / south
|
||||
vns = ((input[7] & 0b01111111) << 3) | ((input[8] & 0b1110000000) >> 5)
|
||||
vx = vew - 1
|
||||
if dew:
|
||||
vx *= -1
|
||||
vy = vns - 1
|
||||
if dns:
|
||||
vy *= -1
|
||||
# supersonic
|
||||
if subtype == 2:
|
||||
vx *= 4
|
||||
vy *= 4
|
||||
message["groundspeed"] = sqrt(vx ** 2 + vy ** 2)
|
||||
message["groundtrack"] = (atan2(vx, vy) * 360 / (2 * pi)) % 360
|
||||
else:
|
||||
logger.debug("subtype: %i", subtype)
|
||||
|
||||
elif type in range(20, 23):
|
||||
# 0 means no data
|
||||
if vew != 0 and vns != 0:
|
||||
# dew = direction east/west (0 = to east, 1 = to west)
|
||||
dew = (input[5] & 0b00000100) >> 2
|
||||
# dns = direction north/south (0 = to north, 1 = to south)
|
||||
dns = (input[7] & 0b10000000) >> 7
|
||||
|
||||
vx = vew - 1
|
||||
if dew:
|
||||
vx *= -1
|
||||
vy = vns - 1
|
||||
if dns:
|
||||
vy *= -1
|
||||
# supersonic
|
||||
if subtype == 2:
|
||||
vx *= 4
|
||||
vy *= 4
|
||||
message["groundspeed"] = sqrt(vx ** 2 + vy ** 2)
|
||||
message["groundtrack"] = (atan2(vx, vy) * 360 / (2 * pi)) % 360
|
||||
|
||||
# vertical rate
|
||||
vr = ((input[8] & 0b00000111) << 6) | ((input[9] & 0b11111100) >> 2)
|
||||
if vr != 0:
|
||||
# vertical speed sign (1 = negative)
|
||||
svr = ((input[8] & 0b00001000) >> 3)
|
||||
# vertical speed
|
||||
vs = 64 * (vr - 1)
|
||||
if svr:
|
||||
vs *= -1
|
||||
message["verticalspeed"] = vs
|
||||
|
||||
elif subtype in [3, 4]:
|
||||
sh = (input[5] & 0b00000100) >> 2
|
||||
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
|
||||
# supersonic
|
||||
if subtype == 4:
|
||||
airspeed *= 4
|
||||
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)
|
||||
if cpr is not None:
|
||||
lat, lon = cpr
|
||||
message["lat"] = lat
|
||||
message["lon"] = lon
|
||||
|
||||
altitude = (input[5] << 4) | ((input[6] & 0b1111) >> 4)
|
||||
message["altitude"] = altitude * FEET_PER_METER
|
||||
|
||||
|
|
@ -92,3 +172,79 @@ class ModeSParser(PickleModule):
|
|||
pass
|
||||
|
||||
return message
|
||||
|
||||
def __getCprData(self, icao: str, input):
|
||||
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])
|
||||
})
|
||||
|
||||
records = self.cprCache.getRecentData(icao)
|
||||
|
||||
try:
|
||||
# records are sorted by timestamp, last should be newest
|
||||
rev = reversed(records)
|
||||
odd = next(r for r in rev if r["cpr_format"])
|
||||
even = next(r for r in rev if not r["cpr_format"])
|
||||
|
||||
lat_cpr_even = even["lat_cpr"] / 2 ** 17
|
||||
lat_cpr_odd = odd["lat_cpr"] / 2 ** 17
|
||||
|
||||
# latitude zone index
|
||||
j = floor(59 * lat_cpr_even - 60 * lat_cpr_odd + .5)
|
||||
|
||||
lat_even = d_lat_even * ((j % 60) + lat_cpr_even)
|
||||
lat_odd = d_lat_odd * ((j % 59) + lat_cpr_odd)
|
||||
|
||||
if lat_even >= 270:
|
||||
lat_even -= 360
|
||||
if lat_odd >= 270:
|
||||
lat_odd -= 360
|
||||
|
||||
def nl(lat):
|
||||
if lat == 0:
|
||||
return 59
|
||||
elif lat == 87:
|
||||
return 2
|
||||
elif lat == -87:
|
||||
return 2
|
||||
elif lat > 87:
|
||||
return 1
|
||||
elif lat < -87:
|
||||
return 1
|
||||
else:
|
||||
return floor((2 * pi) / acos(1 - (1 - cos(pi / (2 * nz))) / (cos((pi / 180) * lat) ** 2)))
|
||||
|
||||
if nl(lat_even) != nl(lat_odd):
|
||||
logger.debug("latitude zone mismatch")
|
||||
return
|
||||
|
||||
newest = next(rev)
|
||||
lat = lat_odd if newest["cpr_format"] else lat_even
|
||||
|
||||
lon_cpr_even = even["lon_cpr"] / 2 ** 17
|
||||
lon_cpr_odd = odd["lon_cpr"] / 2 ** 17
|
||||
|
||||
# longitude zone index
|
||||
nl_lat = nl(lat)
|
||||
m = floor(lon_cpr_even * (nl_lat - 1) - lon_cpr_odd * nl_lat + .5)
|
||||
|
||||
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
|
||||
|
||||
lon_even = d_lon_even * (m % n_even + lon_cpr_even)
|
||||
lon_odd = d_lon_odd * (m % n_odd + lon_cpr_odd)
|
||||
|
||||
lon = lon_odd if newest["cpr_format"] else lon_even
|
||||
if lon >= 180:
|
||||
lon -= 360
|
||||
|
||||
return lat, lon
|
||||
|
||||
except StopIteration:
|
||||
# we don't have both CPR records. better luck next time.
|
||||
pass
|
||||
|
|
|
|||
Loading…
Reference in a new issue