CMakeified almost everything. Test code in python/ and apps other than uhd_modes.py still need minor updating.
This commit is contained in:
53
python/CMakeLists.txt
Normal file
53
python/CMakeLists.txt
Normal file
@@ -0,0 +1,53 @@
|
||||
# Copyright 2011 Free Software Foundation, Inc.
|
||||
#
|
||||
# This file is part of GNU Radio
|
||||
#
|
||||
# GNU Radio is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation; either version 3, or (at your option)
|
||||
# any later version.
|
||||
#
|
||||
# GNU Radio is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with GNU Radio; see the file COPYING. If not, write to
|
||||
# the Free Software Foundation, Inc., 51 Franklin Street,
|
||||
# Boston, MA 02110-1301, USA.
|
||||
|
||||
########################################################################
|
||||
# Include python install macros
|
||||
########################################################################
|
||||
include(GrPython)
|
||||
if(NOT PYTHONINTERP_FOUND)
|
||||
return()
|
||||
endif()
|
||||
|
||||
########################################################################
|
||||
# Install python sources
|
||||
########################################################################
|
||||
GR_PYTHON_INSTALL(
|
||||
FILES
|
||||
__init__.py
|
||||
altitude.py
|
||||
cpr.py
|
||||
mlat.py
|
||||
modes_kml.py
|
||||
modes_parse.py
|
||||
modes_print.py
|
||||
modes_raw_server.py
|
||||
modes_sbs1.py
|
||||
modes_sql.py
|
||||
DESTINATION ${GR_PYTHON_DIR}/air_modes
|
||||
)
|
||||
|
||||
########################################################################
|
||||
# Handle the unit tests
|
||||
########################################################################
|
||||
#include(GrTest)
|
||||
|
||||
#set(GR_TEST_TARGET_DEPS gnuradio-gr-air-modes)
|
||||
#set(GR_TEST_PYTHON_DIRS ${CMAKE_BINARY_DIR}/swig)
|
||||
#GR_ADD_TEST(qa_gr-air-modes ${PYTHON_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/qa_gr-air-modes.py)
|
||||
64
python/__init__.py
Normal file
64
python/__init__.py
Normal file
@@ -0,0 +1,64 @@
|
||||
#
|
||||
# Copyright 2008,2009 Free Software Foundation, Inc.
|
||||
#
|
||||
# This application is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation; either version 3, or (at your option)
|
||||
# any later version.
|
||||
#
|
||||
# This application is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License along
|
||||
# with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
#
|
||||
|
||||
# The presence of this file turns this directory into a Python package
|
||||
|
||||
'''
|
||||
This is the GNU Radio gr-air-modes package. It provides a library and
|
||||
application for receiving Mode S / ADS-B signals from aircraft. Use
|
||||
uhd_modes.py as the main application for receiving signals. cpr.py
|
||||
provides an implementation of Compact Position Reporting. altitude.py
|
||||
implements Gray-coded altitude decoding. Various plugins exist for SQL,
|
||||
KML, and PlanePlotter-compliant SBS-1 emulation output. mlat.py provides
|
||||
an experimental implementation of a multilateration solver.
|
||||
'''
|
||||
|
||||
# ----------------------------------------------------------------
|
||||
# Temporary workaround for ticket:181 (swig+python problem)
|
||||
import sys
|
||||
_RTLD_GLOBAL = 0
|
||||
try:
|
||||
from dl import RTLD_GLOBAL as _RTLD_GLOBAL
|
||||
except ImportError:
|
||||
try:
|
||||
from DLFCN import RTLD_GLOBAL as _RTLD_GLOBAL
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
if _RTLD_GLOBAL != 0:
|
||||
_dlopenflags = sys.getdlopenflags()
|
||||
sys.setdlopenflags(_dlopenflags|_RTLD_GLOBAL)
|
||||
# ----------------------------------------------------------------
|
||||
|
||||
|
||||
# import swig generated symbols into the gr-air-modes namespace
|
||||
from air_modes_swig import *
|
||||
|
||||
# import any pure python here
|
||||
#
|
||||
from modes_print import modes_output_print
|
||||
from modes_sql import modes_output_sql
|
||||
from modes_sbs1 import modes_output_sbs1
|
||||
from modes_kml import modes_kml
|
||||
from modes_raw_server import modes_raw_server
|
||||
|
||||
# ----------------------------------------------------------------
|
||||
# Tail of workaround
|
||||
if _RTLD_GLOBAL != 0:
|
||||
sys.setdlopenflags(_dlopenflags) # Restore original flags
|
||||
# ----------------------------------------------------------------
|
||||
101
python/altitude.py
Normal file
101
python/altitude.py
Normal file
@@ -0,0 +1,101 @@
|
||||
#
|
||||
# Copyright 2010 Nick Foster
|
||||
#
|
||||
# This file is part of gr-air-modes
|
||||
#
|
||||
# gr-air-modes is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation; either version 3, or (at your option)
|
||||
# any later version.
|
||||
#
|
||||
# gr-air-modes is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with gr-air-modes; see the file COPYING. If not, write to
|
||||
# the Free Software Foundation, Inc., 51 Franklin Street,
|
||||
# Boston, MA 02110-1301, USA.
|
||||
#
|
||||
|
||||
|
||||
#!/usr/bin/env python
|
||||
#from string import split, join
|
||||
|
||||
#betcha this would be faster if you used a table for mode C
|
||||
#you could strip out D1 since it's never used, that leaves 11 bits (table is 2048 entries)
|
||||
#on the other hand doing it this way is educational for others
|
||||
def decode_alt(alt, bit13):
|
||||
if alt & 0x40 and bit13 is True:
|
||||
return "METRIC ERROR"
|
||||
|
||||
if alt & 0x10: #a mode S-style reply
|
||||
if bit13 is True:
|
||||
tmp1 = (alt & 0x1F80) >> 2 #first 6 bits get shifted 2 down
|
||||
tmp2 = (alt & 0x20) >> 1 #that bit gets shifted 1 down
|
||||
else:
|
||||
tmp1 = (alt & 0x0FE0) >> 1 #first 7 bits get shifted 1 down but there are only 12 bits in the representation
|
||||
tmp2 = 0
|
||||
|
||||
decoded_alt = ((alt & 0x0F) | tmp1 | tmp2) * 25 - 1000
|
||||
|
||||
else: #a mode C-style reply
|
||||
#okay, the order they come in is:
|
||||
#C1 A1 C2 A2 C4 A4 X B1 D1 B2 D2 B4 D4
|
||||
#the order we want them in is:
|
||||
#D2 D4 A1 A2 A4 B1 B2 B4
|
||||
#so we'll reassemble into a Gray-coded representation
|
||||
|
||||
if bit13 is False:
|
||||
alt = (alt & 0x003F) | (alt & 0x0FC0 << 1)
|
||||
|
||||
C1 = 0x1000
|
||||
A1 = 0x0800
|
||||
C2 = 0x0400
|
||||
A2 = 0x0200 #this represents the order in which the bits come
|
||||
C4 = 0x0100
|
||||
A4 = 0x0080
|
||||
B1 = 0x0020
|
||||
D1 = 0x0010
|
||||
B2 = 0x0008
|
||||
D2 = 0x0004
|
||||
B4 = 0x0002
|
||||
D4 = 0x0001
|
||||
|
||||
bigpart = ((alt & B4) >> 1) \
|
||||
+ ((alt & B2) >> 2) \
|
||||
+ ((alt & B1) >> 3) \
|
||||
+ ((alt & A4) >> 4) \
|
||||
+ ((alt & A2) >> 5) \
|
||||
+ ((alt & A1) >> 6) \
|
||||
+ ((alt & D4) << 6) \
|
||||
+ ((alt & D2) << 5)
|
||||
|
||||
#bigpart is now the 500-foot-resolution Gray-coded binary part
|
||||
decoded_alt = gray2bin(bigpart)
|
||||
#real_alt is now the 500-foot-per-tick altitude
|
||||
|
||||
cbits = ((alt & C4) >> 8) + ((alt & C2) >> 9) + ((alt & C1) >> 10)
|
||||
cval = gray2bin(cbits) #turn them into a real number
|
||||
|
||||
if cval == 7:
|
||||
cval = 5 #not a real gray code after all
|
||||
|
||||
if decoded_alt % 2:
|
||||
cval = 6 - cval #since the code is symmetric this unwraps it to see whether to subtract the C bits or add them
|
||||
|
||||
decoded_alt *= 500 #take care of the A,B,D data
|
||||
decoded_alt += cval * 100 #factor in the C data
|
||||
decoded_alt -= 1300 #subtract the offset
|
||||
|
||||
return decoded_alt
|
||||
|
||||
def gray2bin(gray):
|
||||
i = gray >> 1
|
||||
|
||||
while i != 0:
|
||||
gray ^= i
|
||||
i >>= 1
|
||||
|
||||
return gray
|
||||
63
python/cpr-test.py
Executable file
63
python/cpr-test.py
Executable file
@@ -0,0 +1,63 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# Copyright 2010 Nick Foster
|
||||
#
|
||||
# This file is part of gr-air-modes
|
||||
#
|
||||
# gr-air-modes is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation; either version 3, or (at your option)
|
||||
# any later version.
|
||||
#
|
||||
# gr-air-modes is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with gr-air-modes; see the file COPYING. If not, write to
|
||||
# the Free Software Foundation, Inc., 51 Franklin Street,
|
||||
# Boston, MA 02110-1301, USA.
|
||||
#
|
||||
|
||||
from modes_parse import *
|
||||
from cpr import *
|
||||
import sys
|
||||
|
||||
my_location = [37.76225, -122.44254]
|
||||
|
||||
shortdata = long(sys.argv[1], 16)
|
||||
longdata = long(sys.argv[2], 16)
|
||||
parity = long(sys.argv[3], 16)
|
||||
ecc = long(sys.argv[4], 16)
|
||||
|
||||
parser = modes_parse(my_location)
|
||||
|
||||
[altitude, decoded_lat, decoded_lon, rnge, bearing] = parser.parseBDS06(shortdata, longdata, parity, ecc)
|
||||
|
||||
if decoded_lat is not None:
|
||||
print "Altitude: %i\nLatitude: %.6f\nLongitude: %.6f\nRange: %.2f\nBearing: %i\n" % (altitude, decoded_lat, decoded_lon, rnge, bearing,)
|
||||
|
||||
print "Decomposing...\n"
|
||||
|
||||
subtype = (longdata >> 51) & 0x1F;
|
||||
|
||||
encoded_lon = longdata & 0x1FFFF
|
||||
encoded_lat = (longdata >> 17) & 0x1FFFF
|
||||
cpr_format = (longdata >> 34) & 1
|
||||
|
||||
enc_alt = (longdata >> 36) & 0x0FFF
|
||||
|
||||
[cpr_lat, cpr_lon] = cpr_resolve_local(my_location, [encoded_lat, encoded_lon], cpr_format, 1)
|
||||
|
||||
print "Subtype: %i\nEncoded longitude: %x\nEncoded latitude: %x\nCPR format: %i\nEncoded altitude: %x\n" % (subtype, encoded_lon, encoded_lat, cpr_format, enc_alt,)
|
||||
print "Pos: %.6f %.6f" % (cpr_lat, cpr_lon)
|
||||
|
||||
#print "First argument is order %i, second %i" % ((evendata >> 34) & 1, (odddata >> 34) & 1,)
|
||||
|
||||
#evenencpos = [(evendata >> 17) & 0x1FFFF, evendata & 0x1FFFF]
|
||||
#oddencpos = [(odddata >> 17) & 0x1FFFF, odddata & 0x1FFFF]
|
||||
|
||||
#[declat, declon] = cpr_decode_global(evenencpos, oddencpos, newer)
|
||||
|
||||
#print "Global latitude: %.6f\nGlobal longitude: %.6f" % (declat, declon,)
|
||||
239
python/cpr.py
Normal file
239
python/cpr.py
Normal file
@@ -0,0 +1,239 @@
|
||||
#
|
||||
# Copyright 2010 Nick Foster
|
||||
#
|
||||
# This file is part of gr-air-modes
|
||||
#
|
||||
# gr-air-modes is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation; either version 3, or (at your option)
|
||||
# any later version.
|
||||
#
|
||||
# gr-air-modes is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with gr-air-modes; see the file COPYING. If not, write to
|
||||
# the Free Software Foundation, Inc., 51 Franklin Street,
|
||||
# Boston, MA 02110-1301, USA.
|
||||
#
|
||||
|
||||
|
||||
#!/usr/bin/env python
|
||||
#from string import split, join
|
||||
#from math import pi, floor, cos, acos
|
||||
import math, time
|
||||
#this implements CPR position decoding.
|
||||
|
||||
latz = 15
|
||||
|
||||
def nbits(surface):
|
||||
return 17
|
||||
# if surface == 1:
|
||||
# return 19
|
||||
# else:
|
||||
# return 17
|
||||
|
||||
def nz(ctype):
|
||||
return 4 * latz - ctype
|
||||
|
||||
def dlat(ctype, surface):
|
||||
if surface == 1:
|
||||
tmp = 90.0
|
||||
else:
|
||||
tmp = 360.0
|
||||
|
||||
nzcalc = nz(ctype)
|
||||
if nzcalc == 0:
|
||||
return tmp
|
||||
else:
|
||||
return tmp / nzcalc
|
||||
|
||||
def nl_eo(declat_in, ctype):
|
||||
return nl(declat_in) - ctype
|
||||
|
||||
def nl(declat_in):
|
||||
return math.floor( (2.0*math.pi) * pow(math.acos(1.0- (1.0-math.cos(math.pi/(2.0*latz))) / pow( math.cos( (math.pi/180.0)*abs(declat_in) ) ,2.0) ),-1.0))
|
||||
|
||||
def dlon(declat_in, ctype, surface):
|
||||
if surface == 1:
|
||||
tmp = 90.0
|
||||
else:
|
||||
tmp = 360.0
|
||||
nlcalc = nl_eo(declat_in, ctype)
|
||||
if nlcalc == 0:
|
||||
return tmp
|
||||
else:
|
||||
return tmp / nlcalc
|
||||
|
||||
def decode_lat(enclat, ctype, my_lat, surface):
|
||||
tmp1 = dlat(ctype, surface)
|
||||
tmp2 = float(enclat) / (2**nbits(surface))
|
||||
j = math.floor(my_lat/tmp1) + math.floor(0.5 + (mod(my_lat, tmp1) / tmp1) - tmp2)
|
||||
# print "dlat gives " + "%.6f " % tmp1 + "with j = " + "%.6f " % j + " and tmp2 = " + "%.6f" % tmp2 + " given enclat " + "%x" % enclat
|
||||
|
||||
return tmp1 * (j + tmp2)
|
||||
|
||||
def decode_lon(declat, enclon, ctype, my_lon, surface):
|
||||
tmp1 = dlon(declat, ctype, surface)
|
||||
tmp2 = float(enclon) / (2.0**nbits(surface))
|
||||
m = math.floor(my_lon / tmp1) + math.floor(0.5 + (mod(my_lon, tmp1) / tmp1) - tmp2)
|
||||
# print "dlon gives " + "%.6f " % tmp1 + "with m = " + "%.6f " % m + " and tmp2 = " + "%.6f" % tmp2 + " given enclon " + "%x" % enclon
|
||||
|
||||
return tmp1 * (m + tmp2)
|
||||
|
||||
|
||||
def mod(a, b):
|
||||
if a < 0:
|
||||
a += 360.0
|
||||
|
||||
return a - b * math.floor(a / b)
|
||||
|
||||
def cpr_resolve_local(my_location, encoded_location, ctype, surface):
|
||||
[my_lat, my_lon] = my_location
|
||||
[enclat, enclon] = encoded_location
|
||||
|
||||
decoded_lat = decode_lat(enclat, ctype, my_lat, surface)
|
||||
decoded_lon = decode_lon(decoded_lat, enclon, ctype, my_lon, surface)
|
||||
|
||||
return [decoded_lat, decoded_lon]
|
||||
|
||||
def cpr_resolve_global(evenpos, oddpos, mostrecent, surface): #ok this is considered working, tentatively
|
||||
dlateven = dlat(0, surface);
|
||||
dlatodd = dlat(1, surface);
|
||||
|
||||
# print dlateven;
|
||||
# print dlatodd;
|
||||
|
||||
evenpos = [float(evenpos[0]), float(evenpos[1])]
|
||||
oddpos = [float(oddpos[0]), float(oddpos[1])]
|
||||
|
||||
#print "Even position: %x, %x\nOdd position: %x, %x" % (evenpos[0], evenpos[1], oddpos[0], oddpos[1],)
|
||||
|
||||
j = math.floor(((59*evenpos[0] - 60*oddpos[0])/2**nbits(surface)) + 0.5) #latitude index
|
||||
|
||||
#print "Latitude index: %i" % j #should be 6, getting 5?
|
||||
|
||||
rlateven = dlateven * (mod(j, 60)+evenpos[0]/2**nbits(surface))
|
||||
rlatodd = dlatodd * (mod(j, 59)+ oddpos[0]/2**nbits(surface))
|
||||
|
||||
#print "Rlateven: %f\nRlatodd: %f" % (rlateven, rlatodd,)
|
||||
|
||||
if nl(rlateven) != nl(rlatodd):
|
||||
#print "Boundary straddle!"
|
||||
return (None, None,)
|
||||
|
||||
if mostrecent == 0:
|
||||
rlat = rlateven
|
||||
else:
|
||||
rlat = rlatodd
|
||||
|
||||
if rlat > 90:
|
||||
rlat = rlat - 180.0
|
||||
|
||||
dl = dlon(rlat, mostrecent, surface)
|
||||
nlthing = nl(rlat)
|
||||
ni = nlthing - mostrecent
|
||||
|
||||
#print "ni is %i" % ni
|
||||
|
||||
m = math.floor(((evenpos[1]*(nlthing-1)-oddpos[1]*(nlthing))/2**nbits(surface))+0.5) #longitude index, THIS LINE IS CORRECT
|
||||
#print "m is %f" % m #should be -16
|
||||
|
||||
|
||||
if mostrecent == 0:
|
||||
enclon = evenpos[1]
|
||||
else:
|
||||
enclon = oddpos[1]
|
||||
|
||||
rlon = dl * (mod(ni+m, ni)+enclon/2**nbits(surface))
|
||||
|
||||
if rlon > 180:
|
||||
rlon = rlon - 360.0
|
||||
|
||||
return [rlat, rlon]
|
||||
|
||||
def weed_poslist(poslist):
|
||||
for key, item in poslist.items():
|
||||
if time.time() - item[2] > 900:
|
||||
del poslist[key]
|
||||
|
||||
def cpr_decode(my_location, icao24, encoded_lat, encoded_lon, cpr_format, evenlist, oddlist, lkplist, surface, longdata):
|
||||
#add the info to the position reports list for global decoding
|
||||
if cpr_format==1:
|
||||
oddlist[icao24] = [encoded_lat, encoded_lon, time.time()]
|
||||
else:
|
||||
evenlist[icao24] = [encoded_lat, encoded_lon, time.time()]
|
||||
|
||||
[decoded_lat, decoded_lon] = [None, None]
|
||||
|
||||
#okay, let's traverse the lists and weed out those entries that are older than 15 minutes, as they're unlikely to be useful.
|
||||
weed_poslist(lkplist)
|
||||
weed_poslist(evenlist)
|
||||
weed_poslist(oddlist)
|
||||
|
||||
if surface==1:
|
||||
validrange = 45
|
||||
else:
|
||||
validrange = 180
|
||||
|
||||
if icao24 in lkplist:
|
||||
#do emitter-centered local decoding
|
||||
[decoded_lat, decoded_lon] = cpr_resolve_local(lkplist[icao24][0:2], [encoded_lat, encoded_lon], cpr_format, surface)
|
||||
lkplist[icao24] = [decoded_lat, decoded_lon, time.time()] #update the local position for next time
|
||||
|
||||
elif ((icao24 in evenlist) and (icao24 in oddlist) and abs(evenlist[icao24][2] - oddlist[icao24][2]) < 10):
|
||||
# print "debug: valid even/odd positions, performing global decode."
|
||||
newer = (oddlist[icao24][2] - evenlist[icao24][2]) > 0 #figure out which report is newer
|
||||
[decoded_lat, decoded_lon] = cpr_resolve_global(evenlist[icao24][0:2], oddlist[icao24][0:2], newer, surface) #do a global decode
|
||||
if decoded_lat is not None:
|
||||
lkplist[icao24] = [decoded_lat, decoded_lon, time.time()]
|
||||
|
||||
elif my_location is not None: #if we have a location, use it
|
||||
[local_lat, local_lon] = cpr_resolve_local(my_location, [encoded_lat, encoded_lon], cpr_format, surface) #try local decoding
|
||||
[rnge, bearing] = range_bearing(my_location, [local_lat, local_lon])
|
||||
if rnge < validrange: #if the local decoding can be guaranteed valid
|
||||
lkplist[icao24] = [local_lat, local_lon, time.time()] #update the local position for next time
|
||||
[decoded_lat, decoded_lon] = [local_lat, local_lon]
|
||||
|
||||
|
||||
#print "settled on position: %.6f, %.6f" % (decoded_lat, decoded_lon,)
|
||||
if decoded_lat is not None:
|
||||
[rnge, bearing] = range_bearing(my_location, [decoded_lat, decoded_lon])
|
||||
else:
|
||||
rnge = None
|
||||
bearing = None
|
||||
|
||||
return [decoded_lat, decoded_lon, rnge, bearing]
|
||||
|
||||
|
||||
#calculate range and bearing between two lat/lon points
|
||||
#should probably throw this in the mlat py somewhere or make another lib
|
||||
def range_bearing(loc_a, loc_b):
|
||||
[a_lat, a_lon] = loc_a
|
||||
[b_lat, b_lon] = loc_b
|
||||
|
||||
esquared = (1/298.257223563)*(2-(1/298.257223563))
|
||||
earth_radius_mi = 3963.19059 * (math.pi / 180)
|
||||
|
||||
delta_lat = b_lat - a_lat
|
||||
delta_lon = b_lon - a_lon
|
||||
|
||||
avg_lat = (a_lat + b_lat) / 2.0
|
||||
|
||||
R1 = earth_radius_mi*(1.0-esquared)/pow((1.0-esquared*pow(math.sin(avg_lat),2)),1.5)
|
||||
|
||||
R2 = earth_radius_mi/math.sqrt(1.0-esquared*pow(math.sin(avg_lat),2))
|
||||
|
||||
distance_North = R1*delta_lat
|
||||
distance_East = R2*math.cos(avg_lat)*delta_lon
|
||||
|
||||
bearing = math.atan2(distance_East,distance_North) * (180.0 / math.pi)
|
||||
if bearing < 0.0:
|
||||
bearing += 360.0
|
||||
|
||||
rnge = math.hypot(distance_East,distance_North)
|
||||
|
||||
|
||||
return [rnge, bearing]
|
||||
27
python/mlat-test.py
Executable file
27
python/mlat-test.py
Executable file
@@ -0,0 +1,27 @@
|
||||
#!/usr/bin/python
|
||||
import mlat
|
||||
import numpy
|
||||
|
||||
#here's some test data to validate the algorithm
|
||||
teststations = [[37.76225, -122.44254, 100], [37.680016,-121.772461, 100], [37.385844,-122.083082, 100], [37.701207,-122.309418, 100]]
|
||||
testalt = 8000
|
||||
testplane = numpy.array(mlat.llh2ecef([37.617175,-122.400843, testalt]))
|
||||
testme = mlat.llh2geoid(teststations[0])
|
||||
teststamps = [10,
|
||||
10 + numpy.linalg.norm(testplane-numpy.array(mlat.llh2geoid(teststations[1]))) / mlat.c,
|
||||
10 + numpy.linalg.norm(testplane-numpy.array(mlat.llh2geoid(teststations[2]))) / mlat.c,
|
||||
10 + numpy.linalg.norm(testplane-numpy.array(mlat.llh2geoid(teststations[3]))) / mlat.c,
|
||||
]
|
||||
|
||||
print teststamps
|
||||
|
||||
replies = []
|
||||
for i in range(0, len(teststations)):
|
||||
replies.append((teststations[i], teststamps[i]))
|
||||
ans = mlat.mlat(replies, testalt)
|
||||
error = numpy.linalg.norm(numpy.array(mlat.llh2ecef(ans))-numpy.array(testplane))
|
||||
range = numpy.linalg.norm(mlat.llh2geoid(ans)-numpy.array(testme))
|
||||
print testplane-testme
|
||||
print ans
|
||||
print "Error: %.2fm" % (error)
|
||||
print "Range: %.2fkm (from first station in list)" % (range/1000)
|
||||
173
python/mlat.py
Executable file
173
python/mlat.py
Executable file
@@ -0,0 +1,173 @@
|
||||
#!/usr/bin/python
|
||||
import math
|
||||
import numpy
|
||||
from scipy.ndimage import map_coordinates
|
||||
|
||||
#functions for multilateration.
|
||||
#this library is more or less based around the so-called "GPS equation", the canonical
|
||||
#iterative method for getting position from GPS satellite time difference of arrival data.
|
||||
#here, instead of multiple orbiting satellites with known time reference and position,
|
||||
#we have multiple fixed stations with known time references (GPSDO, hopefully) and known
|
||||
#locations (again, GPSDO).
|
||||
|
||||
#NB: because of the way this solver works, at least 3 stations and timestamps
|
||||
#are required. this function will not return hyperbolae for underconstrained systems.
|
||||
#TODO: get HDOP out of this so we can draw circles of likely position and indicate constraint
|
||||
########################END NOTES#######################################
|
||||
|
||||
|
||||
#this is a 10x10-degree WGS84 geoid datum, in meters relative to the WGS84 reference ellipsoid. given the maximum slope, you should probably interpolate.
|
||||
#NIMA suggests a 2x2 interpolation using four neighbors. we'll go cubic spline JUST BECAUSE WE CAN
|
||||
wgs84_geoid = numpy.array([[13,13,13,13,13,13,13,13,13,13,13,13,13,13,13,13,13,13,13,13,13,13,13,13,13,13,13,13,13,13,13,13,13,13,13,13], #90N
|
||||
[3,1,-2,-3,-3,-3,-1,3,1,5,9,11,19,27,31,34,33,34,33,34,28,23,17,13,9,4,4,1,-2,-2,0,2,3,2,1,1], #80N
|
||||
[2,2,1,-1,-3,-7,-14,-24,-27,-25,-19,3,24,37,47,60,61,58,51,43,29,20,12,5,-2,-10,-14,-12,-10,-14,-12,-6,-2,3,6,4], #70N
|
||||
[2,9,17,10,13,1,-14,-30,-39,-46,-42,-21,6,29,49,65,60,57,47,41,21,18,14,7,-3,-22,-29,-32,-32,-26,-15,-2,13,17,19,6], #60N
|
||||
[-8,8,8,1,-11,-19,-16,-18,-22,-35,-40,-26,-12,24,45,63,62,59,47,48,42,28,12,-10,-19,-33,-43,-42,-43,-29,-2,17,23,22,6,2], #50N
|
||||
[-12,-10,-13,-20,-31,-34,-21,-16,-26,-34,-33,-35,-26,2,33,59,52,51,52,48,35,40,33,-9,-28,-39,-48,-59,-50,-28,3,23,37,18,-1,-11], #40N
|
||||
[-7,-5,-8,-15,-28,-40,-42,-29,-22,-26,-32,-51,-40,-17,17,31,34,44,36,28,29,17,12,-20,-15,-40,-33,-34,-34,-28,7,29,43,20,4,-6], #30N
|
||||
[5,10,7,-7,-23,-39,-47,-34,-9,-10,-20,-45,-48,-32,-9,17,25,31,31,26,15,6,1,-29,-44,-61,-67,-59,-36,-11,21,39,49,39,22,10], #20N
|
||||
[13,12,11,2,-11,-28,-38,-29,-10,3,1,-11,-41,-42,-16,3,17,33,22,23,2,-3,-7,-36,-59,-90,-95,-63,-24,12,53,60,58,46,36,26], #10N
|
||||
[22,16,17,13,1,-12,-23,-20,-14,-3,14,10,-15,-27,-18,3,12,20,18,12,-13,-9,-28,-49,-62,-89,-102,-63,-9,33,58,73,74,63,50,32], #0
|
||||
[36,22,11,6,-1,-8,-10,-8,-11,-9,1,32,4,-18,-13,-9,4,14,12,13,-2,-14,-25,-32,-38,-60,-75,-63,-26,0,35,52,68,76,64,52], #10S
|
||||
[51,27,10,0,-9,-11,-5,-2,-3,-1,9,35,20,-5,-6,-5,0,13,17,23,21,8,-9,-10,-11,-20,-40,-47,-45,-25,5,23,45,58,57,63], #20S
|
||||
[46,22,5,-2,-8,-13,-10,-7,-4,1,9,32,16,4,-8,4,12,15,22,27,34,29,14,15,15,7,-9,-25,-37,-39,-23,-14,15,33,34,45], #30S
|
||||
[21,6,1,-7,-12,-12,-12,-10,-7,-1,8,23,15,-2,-6,6,21,24,18,26,31,33,39,41,30,24,13,-2,-20,-32,-33,-27,-14,-2,5,20], #40S
|
||||
[-15,-18,-18,-16,-17,-15,-10,-10,-8,-2,6,14,13,3,3,10,20,27,25,26,34,39,45,45,38,39,28,13,-1,-15,-22,-22,-18,-15,-14,-10], #50S
|
||||
[-45,-43,-37,-32,-30,-26,-23,-22,-16,-10,-2,10,20,20,21,24,22,17,16,19,25,30,35,35,33,30,27,10,-2,-14,-23,-30,-33,-29,-35,-43], #60S
|
||||
[-61,-60,-61,-55,-49,-44,-38,-31,-25,-16,-6,1,4,5,4,2,6,12,16,16,17,21,20,26,26,22,16,10,-1,-16,-29,-36,-46,-55,-54,-59], #70S
|
||||
[-53,-54,-55,-52,-48,-42,-38,-38,-29,-26,-26,-24,-23,-21,-19,-16,-12,-8,-4,-1,1,4,4,6,5,4,2,-6,-15,-24,-33,-40,-48,-50,-53,-52], #80S
|
||||
[-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30,-30]], #90S
|
||||
dtype=numpy.float)
|
||||
|
||||
#ok this calculates the geoid offset from the reference ellipsoid
|
||||
#combined with LLH->ECEF this gets you XYZ for a ground-referenced point
|
||||
def wgs84_height(lat, lon):
|
||||
yi = numpy.array([9-lat/10.0])
|
||||
xi = numpy.array([18+lon/10.0])
|
||||
return float(map_coordinates(wgs84_geoid, [yi, xi]))
|
||||
|
||||
#WGS84 reference ellipsoid constants
|
||||
wgs84_a = 6378137.0
|
||||
wgs84_b = 6356752.314245
|
||||
wgs84_e2 = 0.0066943799901975848
|
||||
wgs84_a2 = wgs84_a**2 #to speed things up a bit
|
||||
wgs84_b2 = wgs84_b**2
|
||||
|
||||
#convert ECEF to lat/lon/alt without geoid correction
|
||||
#returns alt in meters
|
||||
def ecef2llh((x,y,z)):
|
||||
ep = math.sqrt((wgs84_a2 - wgs84_b2) / wgs84_b2)
|
||||
p = math.sqrt(x**2+y**2)
|
||||
th = math.atan2(wgs84_a*z, wgs84_b*p)
|
||||
lon = math.atan2(y, x)
|
||||
lat = math.atan2(z+ep**2*wgs84_b*math.sin(th)**3, p-wgs84_e2*wgs84_a*math.cos(th)**3)
|
||||
N = wgs84_a / math.sqrt(1-wgs84_e2*math.sin(lat)**2)
|
||||
alt = p / math.cos(lat) - N
|
||||
|
||||
lon *= (180. / math.pi)
|
||||
lat *= (180. / math.pi)
|
||||
|
||||
return [lat, lon, alt]
|
||||
|
||||
#convert lat/lon/alt coords to ECEF without geoid correction, WGS84 model
|
||||
#remember that alt is in meters
|
||||
def llh2ecef((lat, lon, alt)):
|
||||
lat *= (math.pi / 180.0)
|
||||
lon *= (math.pi / 180.0)
|
||||
|
||||
n = lambda x: wgs84_a / math.sqrt(1 - wgs84_e2*(math.sin(x)**2))
|
||||
|
||||
x = (n(lat) + alt)*math.cos(lat)*math.cos(lon)
|
||||
y = (n(lat) + alt)*math.cos(lat)*math.sin(lon)
|
||||
z = (n(lat)*(1-wgs84_e2)+alt)*math.sin(lat)
|
||||
|
||||
return [x,y,z]
|
||||
|
||||
#do both of the above to get a geoid-corrected x,y,z position
|
||||
def llh2geoid((lat, lon, alt)):
|
||||
(x,y,z) = llh2ecef((lat, lon, alt + wgs84_height(lat, lon)))
|
||||
return [x,y,z]
|
||||
|
||||
|
||||
c = 299792458 / 1.0003 #modified for refractive index of air, why not
|
||||
|
||||
#this function is the iterative solver core of the mlat function below
|
||||
#we use limit as a goal to stop solving when we get "close enough" (error magnitude in meters for that iteration)
|
||||
#basically 20 meters is way less than the anticipated error of the system so it doesn't make sense to continue
|
||||
#it's possible this could fail in situations where the solution converges slowly
|
||||
#TODO: this fails to converge for some seriously advantageous geometry
|
||||
def mlat_iter(rel_stations, prange_obs, xguess = [0,0,0], limit = 20, maxrounds = 100):
|
||||
xerr = [1e9, 1e9, 1e9]
|
||||
rounds = 0
|
||||
while numpy.linalg.norm(xerr) > limit:
|
||||
prange_est = [[numpy.linalg.norm(station - xguess)] for station in rel_stations]
|
||||
dphat = prange_obs - prange_est
|
||||
H = numpy.array([(numpy.array(-rel_stations[row,:])+xguess) / prange_est[row] for row in range(0,len(rel_stations))])
|
||||
#now we have H, the Jacobian, and can solve for residual error
|
||||
xerr = numpy.linalg.lstsq(H, dphat)[0].flatten()
|
||||
xguess += xerr
|
||||
#print xguess, xerr
|
||||
rounds += 1
|
||||
if rounds > maxrounds:
|
||||
raise Exception("Failed to converge!")
|
||||
break
|
||||
return xguess
|
||||
|
||||
#func mlat:
|
||||
#uses a modified GPS pseudorange solver to locate aircraft by multilateration.
|
||||
#replies is a list of reports, in ([lat, lon, alt], timestamp) format
|
||||
#altitude is the barometric altitude of the aircraft as returned by the aircraft
|
||||
#returns the estimated position of the aircraft in (lat, lon, alt) geoid-corrected WGS84.
|
||||
#let's make it take a list of tuples so we can sort by them
|
||||
def mlat(replies, altitude):
|
||||
sorted_replies = sorted(replies, key=lambda time: time[1])
|
||||
|
||||
stations = [sorted_reply[0] for sorted_reply in sorted_replies]
|
||||
timestamps = [sorted_reply[1] for sorted_reply in sorted_replies]
|
||||
|
||||
me_llh = stations[0]
|
||||
me = llh2geoid(stations[0])
|
||||
|
||||
|
||||
#list of stations in XYZ relative to me
|
||||
rel_stations = [numpy.array(llh2geoid(station)) - numpy.array(me) for station in stations[1:]]
|
||||
rel_stations.append([0,0,0] - numpy.array(me))
|
||||
rel_stations = numpy.array(rel_stations) #convert list of arrays to 2d array
|
||||
|
||||
#differentiate the timestamps to get TDOA, multiply by c to get pseudorange
|
||||
prange_obs = [[c*(stamp-timestamps[0])] for stamp in timestamps[1:]]
|
||||
|
||||
#so here we calc the estimated pseudorange to the center of the earth, using station[0] as a reference point for the geoid
|
||||
#in other words, we say "if the aircraft were directly overhead of station[0], this is the prange to the center of the earth"
|
||||
#this is a necessary approximation since we don't know the location of the aircraft yet
|
||||
#if the dang earth were actually round this wouldn't be an issue
|
||||
prange_obs.append( [numpy.linalg.norm(llh2ecef((me_llh[0], me_llh[1], altitude)))] ) #use ECEF not geoid since alt is MSL not GPS
|
||||
prange_obs = numpy.array(prange_obs)
|
||||
|
||||
#xguess = llh2ecef([37.617175,-122.400843, 8000])-numpy.array(me)
|
||||
#xguess = [0,0,0]
|
||||
#start our guess directly overhead, who cares
|
||||
xguess = numpy.array(llh2ecef([me_llh[0], me_llh[1], altitude])) - numpy.array(me)
|
||||
|
||||
xyzpos = mlat_iter(rel_stations, prange_obs, xguess)
|
||||
llhpos = ecef2llh(xyzpos+me)
|
||||
|
||||
#now, we could return llhpos right now and be done with it.
|
||||
#but the assumption we made above, namely that the aircraft is directly above the
|
||||
#nearest station, results in significant error due to the oblateness of the Earth's geometry.
|
||||
#so now we solve AGAIN, but this time with the corrected pseudorange of the aircraft altitude
|
||||
#this might not be really useful in practice but the sim shows >50m errors without it
|
||||
#and <4cm errors with it, not that we'll get that close in reality but hey let's do it right
|
||||
prange_obs[-1] = [numpy.linalg.norm(llh2ecef((llhpos[0], llhpos[1], altitude)))]
|
||||
xyzpos_corr = mlat_iter(rel_stations, prange_obs, xyzpos) #start off with a really close guess
|
||||
llhpos = ecef2llh(xyzpos_corr+me)
|
||||
|
||||
#and now, what the hell, let's try to get dilution of precision data
|
||||
#avec is the unit vector of relative ranges to the aircraft from each of the stations
|
||||
# for i in range(len(avec)):
|
||||
# avec[i] = numpy.array(avec[i]) / numpy.linalg.norm(numpy.array(avec[i]))
|
||||
# numpy.append(avec, [[-1],[-1],[-1],[-1]], 1) #must be # of stations
|
||||
# doparray = numpy.linalg.inv(avec.T*avec)
|
||||
#the diagonal elements of doparray will be the x, y, z DOPs.
|
||||
|
||||
return llhpos
|
||||
154
python/modes_kml.py
Normal file
154
python/modes_kml.py
Normal file
@@ -0,0 +1,154 @@
|
||||
#
|
||||
# Copyright 2010 Nick Foster
|
||||
#
|
||||
# This file is part of gr-air-modes
|
||||
#
|
||||
# gr-air-modes is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation; either version 3, or (at your option)
|
||||
# any later version.
|
||||
#
|
||||
# gr-air-modes is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with gr-air-modes; see the file COPYING. If not, write to
|
||||
# the Free Software Foundation, Inc., 51 Franklin Street,
|
||||
# Boston, MA 02110-1301, USA.
|
||||
#
|
||||
|
||||
import sqlite3
|
||||
import string, math, threading, time
|
||||
|
||||
class modes_kml(threading.Thread):
|
||||
def __init__(self, dbfile, filename, localpos, timeout=5):
|
||||
threading.Thread.__init__(self)
|
||||
self._filename = filename
|
||||
self._dbfile = dbfile
|
||||
self.my_coords = localpos
|
||||
self._timeout = timeout
|
||||
self.done = False
|
||||
self.setDaemon(1)
|
||||
self.start()
|
||||
|
||||
def run(self):
|
||||
while self.done is False:
|
||||
self.output()
|
||||
time.sleep(self._timeout)
|
||||
|
||||
self.done = True
|
||||
|
||||
def output(self):
|
||||
self._db = sqlite3.connect(self._dbfile)
|
||||
kmlstr = self.genkml()
|
||||
if kmlstr is not None:
|
||||
f = open(self._filename, 'w')
|
||||
f.write(kmlstr)
|
||||
f.close()
|
||||
self._db.close()
|
||||
|
||||
def draw_circle(self, center, rng):
|
||||
retstr = ""
|
||||
steps=30
|
||||
#so we're going to do this by computing a bearing angle based on the steps, and then compute the coordinate of a line extended from the center point to that range.
|
||||
[center_lat, center_lon] = center
|
||||
esquared = (1/298.257223563)*(2-(1/298.257223563))
|
||||
earth_radius_mi = 3963.19059
|
||||
|
||||
#here we figure out the circumference of the latitude ring
|
||||
#which tells us how wide one line of longitude is at our latitude
|
||||
lat_circ = earth_radius_mi * math.cos(center_lat)
|
||||
#the circumference of the longitude ring will be equal to the circumference of the earth
|
||||
|
||||
lat_rad = math.radians(center_lat)
|
||||
lon_rad = math.radians(center_lon)
|
||||
|
||||
tmp0 = rng / earth_radius_mi
|
||||
|
||||
for i in range(0, steps+1):
|
||||
bearing = i*(2*math.pi/steps) #in radians
|
||||
lat_out = math.degrees(math.asin(math.sin(lat_rad)*math.cos(tmp0) + math.cos(lat_rad)*math.sin(tmp0)*math.cos(bearing)))
|
||||
lon_out = center_lon + math.degrees(math.atan2(math.sin(bearing)*math.sin(tmp0)*math.cos(lat_rad), math.cos(tmp0)-math.sin(lat_rad)*math.sin(math.radians(lat_out))))
|
||||
retstr += " %.8f, %.8f, 0" % (lon_out, lat_out,)
|
||||
|
||||
retstr = string.lstrip(retstr)
|
||||
return retstr
|
||||
|
||||
def genkml(self):
|
||||
#first let's draw the static content
|
||||
retstr="""<?xml version="1.0" encoding="UTF-8"?>\n<kml xmlns="http://www.opengis.net/kml/2.2">\n<Document>\n\t<Style id="airplane">\n\t\t<IconStyle>\n\t\t\t<Icon><href>airports.png</href></Icon>\n\t\t</IconStyle>\n\t</Style>\n\t<Style id="rangering">\n\t<LineStyle>\n\t\t<color>9f4f4faf</color>\n\t\t<width>2</width>\n\t</LineStyle>\n\t</Style>\n\t<Style id="track">\n\t<LineStyle>\n\t\t<color>5fff8f8f</color>\n\t\t<width>4</width>\n\t</LineStyle>\n\t</Style>"""
|
||||
retstr += """\t<Folder>\n\t\t<name>Range rings</name>\n\t\t<open>0</open>"""
|
||||
|
||||
for rng in [100, 200, 300]:
|
||||
retstr += """\n\t\t<Placemark>\n\t\t\t<name>%inm</name>\n\t\t\t<styleUrl>#rangering</styleUrl>\n\t\t\t<LinearRing>\n\t\t\t\t<coordinates>%s</coordinates>\n\t\t\t</LinearRing>\n\t\t</Placemark>""" % (rng, self.draw_circle(self.my_coords, rng),)
|
||||
|
||||
retstr += """\t</Folder>\n\t<Folder>\n\t\t<name>Aircraft locations</name>\n\t\t<open>0</open>"""
|
||||
|
||||
#read the database and add KML
|
||||
q = "select distinct icao from positions where seen > datetime('now', '-5 minute')"
|
||||
c = self._db.cursor()
|
||||
c.execute(q)
|
||||
icaolist = c.fetchall()
|
||||
#now we have a list icaolist of all ICAOs seen in the last 5 minutes
|
||||
|
||||
for icao in icaolist:
|
||||
#print "ICAO: %x" % icao
|
||||
q = "select * from positions where icao=%i and seen > datetime('now', '-2 hour') ORDER BY seen DESC" % icao
|
||||
c.execute(q)
|
||||
track = c.fetchall()
|
||||
#print "Track length: %i" % len(track)
|
||||
if len(track) != 0:
|
||||
lat = track[0][3]
|
||||
if lat is None: lat = 0
|
||||
lon = track[0][4]
|
||||
if lon is None: lon = 0
|
||||
alt = track[0][2]
|
||||
if alt is None: alt = 0
|
||||
|
||||
metric_alt = alt * 0.3048 #google earth takes meters, the commie bastards
|
||||
|
||||
trackstr = ""
|
||||
|
||||
for pos in track:
|
||||
trackstr += " %f, %f, %f" % (pos[4], pos[3], pos[2]*0.3048)
|
||||
|
||||
trackstr = string.lstrip(trackstr)
|
||||
else:
|
||||
alt = 0
|
||||
metric_alt = 0
|
||||
lat = 0
|
||||
lon = 0
|
||||
trackstr = str("")
|
||||
|
||||
#now get metadata
|
||||
q = "select ident from ident where icao=%i" % icao
|
||||
c.execute(q)
|
||||
r = c.fetchall()
|
||||
if len(r) != 0:
|
||||
ident = r[0][0]
|
||||
else: ident=""
|
||||
#if ident is None: ident = ""
|
||||
#get most recent speed/heading/vertical
|
||||
q = "select seen, speed, heading, vertical from vectors where icao=%i order by seen desc limit 1" % icao
|
||||
c.execute(q)
|
||||
r = c.fetchall()
|
||||
if len(r) != 0:
|
||||
seen = r[0][0]
|
||||
speed = r[0][1]
|
||||
heading = r[0][2]
|
||||
vertical = r[0][3]
|
||||
|
||||
else:
|
||||
seen = 0
|
||||
speed = 0
|
||||
heading = 0
|
||||
vertical = 0
|
||||
#now generate some KML
|
||||
retstr+= "\n\t\t<Placemark>\n\t\t\t<name>%s</name>\n\t\t\t<styleUrl>#airplane</styleUrl>\n\t\t\t<description>\n\t\t\t\t<![CDATA[Altitude: %s<br/>Heading: %i<br/>Speed: %i<br/>Vertical speed: %i<br/>ICAO: %x<br/>Last seen: %s]]>\n\t\t\t</description>\n\t\t\t<Point>\n\t\t\t\t<altitudeMode>absolute</altitudeMode>\n\t\t\t\t<extrude>1</extrude>\n\t\t\t\t<coordinates>%s,%s,%i</coordinates>\n\t\t\t</Point>\n\t\t</Placemark>" % (ident, alt, heading, speed, vertical, icao[0], seen, lon, lat, metric_alt, )
|
||||
|
||||
retstr+= "\n\t\t<Placemark>\n\t\t\t<styleUrl>#track</styleUrl>\n\t\t\t<LineString>\n\t\t\t\t<extrude>0</extrude>\n\t\t\t\t<altitudeMode>absolute</altitudeMode>\n\t\t\t\t<coordinates>%s</coordinates>\n\t\t\t</LineString>\n\t\t</Placemark>" % (trackstr,)
|
||||
|
||||
retstr+= '\n\t</Folder>\n</Document>\n</kml>'
|
||||
return retstr
|
||||
264
python/modes_parse.py
Normal file
264
python/modes_parse.py
Normal file
@@ -0,0 +1,264 @@
|
||||
#
|
||||
# Copyright 2010 Nick Foster
|
||||
#
|
||||
# This file is part of gr-air-modes
|
||||
#
|
||||
# gr-air-modes is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation; either version 3, or (at your option)
|
||||
# any later version.
|
||||
#
|
||||
# gr-air-modes is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with gr-air-modes; see the file COPYING. If not, write to
|
||||
# the Free Software Foundation, Inc., 51 Franklin Street,
|
||||
# Boston, MA 02110-1301, USA.
|
||||
#
|
||||
|
||||
|
||||
import time, os, sys
|
||||
from string import split, join
|
||||
from altitude import decode_alt
|
||||
from cpr import cpr_decode
|
||||
import math
|
||||
|
||||
class modes_parse:
|
||||
def __init__(self, mypos):
|
||||
self.my_location = mypos
|
||||
|
||||
def parse0(self, shortdata, parity, ecc):
|
||||
# shortdata = long(shortdata, 16)
|
||||
#parity = long(parity)
|
||||
|
||||
vs = bool(shortdata >> 26 & 0x1) #ground sensor -- airborne when 0
|
||||
cc = bool(shortdata >> 25 & 0x1) #crosslink capability, binary
|
||||
sl = shortdata >> 21 & 0x07 #operating sensitivity of onboard TCAS system. 0 means no TCAS sensitivity reported, 1-7 give TCAS sensitivity
|
||||
ri = shortdata >> 15 & 0x0F #speed coding: 0 = no onboard TCAS, 1 = NA, 2 = TCAS w/inhib res, 3 = TCAS w/vert only, 4 = TCAS w/vert+horiz, 5-7 = NA, 8 = no max A/S avail,
|
||||
#9 = A/S <= 75kt, 10 = A/S (75-150]kt, 11 = (150-300]kt, 12 = (300-600]kt, 13 = (600-1200]kt, 14 = >1200kt, 15 = NA
|
||||
|
||||
altitude = decode_alt(shortdata & 0x1FFF, True) #bit 13 is set for type 0
|
||||
|
||||
return [vs, cc, sl, ri, altitude]
|
||||
|
||||
def parse4(self, shortdata, parity, ecc):
|
||||
# shortdata = long(shortdata, 16)
|
||||
fs = shortdata >> 24 & 0x07 #flight status: 0 is airborne normal, 1 is ground normal, 2 is airborne alert, 3 is ground alert, 4 is alert SPI, 5 is normal SPI
|
||||
dr = shortdata >> 19 & 0x1F #downlink request: 0 means no req, bit 0 is Comm-B msg rdy bit, bit 1 is TCAS info msg rdy, bit 2 is Comm-B bcast #1 msg rdy, bit2+bit0 is Comm-B bcast #2 msg rdy,
|
||||
#bit2+bit1 is TCAS info and Comm-B bcast #1 msg rdy, bit2+bit1+bit0 is TCAS info and Comm-B bcast #2 msg rdy, 8-15 N/A, 16-31 req to send N-15 segments
|
||||
um = shortdata >> 13 & 0x3F #transponder status readouts, no decoding information available
|
||||
|
||||
altitude = decode_alt(shortdata & 0x1FFF, True)
|
||||
|
||||
return [fs, dr, um, altitude]
|
||||
|
||||
|
||||
|
||||
def parse5(self, shortdata, parity, ecc):
|
||||
# shortdata = long(shortdata, 16)
|
||||
fs = shortdata >> 24 & 0x07 #flight status: 0 is airborne normal, 1 is ground normal, 2 is airborne alert, 3 is ground alert, 4 is alert SPI, 5 is normal SPI
|
||||
dr = shortdata >> 19 & 0x1F #downlink request: 0 means no req, bit 0 is Comm-B msg rdy bit, bit 1 is TCAS info msg rdy, bit 2 is Comm-B bcast #1 msg rdy, bit2+bit0 is Comm-B bcast #2 msg rdy,
|
||||
#bit2+bit1 is TCAS info and Comm-B bcast #1 msg rdy, bit2+bit1+bit0 is TCAS info and Comm-B bcast #2 msg rdy, 8-15 N/A, 16-31 req to send N-15 segments
|
||||
um = shortdata >> 13 & 0x3F #transponder status readouts, no decoding information available
|
||||
|
||||
return [fs, dr, um]
|
||||
|
||||
def parse11(self, shortdata, parity, ecc):
|
||||
# shortdata = long(shortdata, 16)
|
||||
interrogator = ecc & 0x0F
|
||||
|
||||
ca = shortdata >> 13 & 0x3F #capability
|
||||
icao24 = shortdata & 0xFFFFFF
|
||||
|
||||
return [icao24, interrogator, ca]
|
||||
|
||||
#def parse17(self, shortdata, longdata, parity, ecc):
|
||||
# shortdata = long(shortdata, 16)
|
||||
# longdata = long(longdata, 16)
|
||||
# parity = long(parity, 16)
|
||||
# ecc = long(ecc, 16)
|
||||
|
||||
# subtype = (longdata >> 51) & 0x1F;
|
||||
|
||||
#the subtypes are:
|
||||
#0: No position information
|
||||
#1: Identification (Category set D)
|
||||
#2: Identification (Category set C)
|
||||
#3: "" (B)
|
||||
#4: "" (A)
|
||||
#5: Surface position accurate to 7.5m
|
||||
#6: "" to 25m
|
||||
#7: "" to 185.2m (0.1nm)
|
||||
#8: "" above 185.2m
|
||||
#9: Airborne position to 7.5m
|
||||
#10-18: Same with less accuracy
|
||||
#19: Airborne velocity
|
||||
#20: Airborne position w/GNSS height above earth
|
||||
#21: same to 25m
|
||||
#22: same above 25m
|
||||
#23: Reserved
|
||||
#24: Reserved for surface system status
|
||||
#25-27: Reserved
|
||||
#28: Extended squitter aircraft status
|
||||
#29: Current/next trajectory change point
|
||||
#30: Aircraft operational coordination
|
||||
#31: Aircraft operational status
|
||||
|
||||
|
||||
# if subtype == 4:
|
||||
# retstr = parseBDS08(shortdata, longdata, parity, ecc)
|
||||
# elif subtype >= 9 and subtype <= 18:
|
||||
# retstr = parseBDS05(shortdata, longdata, parity, ecc)
|
||||
# elif subtype == 19:
|
||||
# subsubtype = (longdata >> 48) & 0x07
|
||||
# if subsubtype == 0:
|
||||
# retstr = parseBDS09_0(shortdata, longdata, parity, ecc)
|
||||
# elif subsubtype == 1:
|
||||
# retstr = parseBDS09_1(shortdata, longdata, parity, ecc)
|
||||
# else:
|
||||
# retstr = "BDS09 subtype " + str(subsubtype) + " not implemented"
|
||||
# else:
|
||||
# retstr = "Type 17, subtype " + str(subtype) + " not implemented"
|
||||
|
||||
# return retstr
|
||||
|
||||
def parseBDS08(self, shortdata, longdata, parity, ecc):
|
||||
icao24 = shortdata & 0xFFFFFF
|
||||
|
||||
msg = ""
|
||||
for i in range(0, 8):
|
||||
msg += self.charmap( longdata >> (42-6*i) & 0x3F)
|
||||
|
||||
#retstr = "Type 17 subtype 04 (ident) from " + "%x" % icao24 + " with data " + msg
|
||||
|
||||
return msg
|
||||
|
||||
def charmap(self, d):
|
||||
if d > 0 and d < 27:
|
||||
retval = chr(ord("A")+d-1)
|
||||
elif d == 32:
|
||||
retval = " "
|
||||
elif d > 47 and d < 58:
|
||||
retval = chr(ord("0")+d-48)
|
||||
else:
|
||||
retval = " "
|
||||
|
||||
return retval
|
||||
|
||||
#lkplist is the last known position, for emitter-centered decoding. evenlist and oddlist are the last
|
||||
#received encoded position data for each reporting type. all dictionaries indexed by ICAO number.
|
||||
_lkplist = {}
|
||||
_evenlist = {}
|
||||
_oddlist = {}
|
||||
_evenlist_ground = {}
|
||||
_oddlist_ground = {}
|
||||
|
||||
#the above dictionaries are all in the format [lat, lon, time].
|
||||
|
||||
def parseBDS05(self, shortdata, longdata, parity, ecc):
|
||||
icao24 = shortdata & 0xFFFFFF
|
||||
|
||||
encoded_lon = longdata & 0x1FFFF
|
||||
encoded_lat = (longdata >> 17) & 0x1FFFF
|
||||
cpr_format = (longdata >> 34) & 1
|
||||
|
||||
enc_alt = (longdata >> 36) & 0x0FFF
|
||||
|
||||
altitude = decode_alt(enc_alt, False)
|
||||
|
||||
[decoded_lat, decoded_lon, rnge, bearing] = cpr_decode(self.my_location, icao24, encoded_lat, encoded_lon, cpr_format, self._evenlist, self._oddlist, self._lkplist, 0, longdata)
|
||||
|
||||
return [altitude, decoded_lat, decoded_lon, rnge, bearing]
|
||||
|
||||
|
||||
#welp turns out it looks like there's only 17 bits in the BDS0,6 ground packet after all. fuck.
|
||||
def parseBDS06(self, shortdata, longdata, parity, ecc):
|
||||
icao24 = shortdata & 0xFFFFFF
|
||||
|
||||
encoded_lon = longdata & 0x1FFFF
|
||||
encoded_lat = (longdata >> 17) & 0x1FFFF
|
||||
cpr_format = (longdata >> 34) & 1
|
||||
|
||||
# enc_alt = (longdata >> 36) & 0x0FFF
|
||||
|
||||
altitude = 0
|
||||
|
||||
[decoded_lat, decoded_lon, rnge, bearing] = cpr_decode(self.my_location, icao24, encoded_lat, encoded_lon, cpr_format, self._evenlist_ground, self._oddlist_ground, self._lkplist, 1, longdata)
|
||||
|
||||
return [altitude, decoded_lat, decoded_lon, rnge, bearing]
|
||||
|
||||
def parseBDS09_0(self, shortdata, longdata, parity, ecc):
|
||||
icao24 = shortdata & 0xFFFFFF
|
||||
vert_spd = ((longdata >> 6) & 0x1FF) * 32
|
||||
ud = bool((longdata >> 15) & 1)
|
||||
if ud:
|
||||
vert_spd = 0 - vert_spd
|
||||
turn_rate = (longdata >> 16) & 0x3F
|
||||
turn_rate = turn_rate * 15/62
|
||||
rl = bool((longdata >> 22) & 1)
|
||||
if rl:
|
||||
turn_rate = 0 - turn_rate
|
||||
ns_vel = (longdata >> 23) & 0x7FF - 1
|
||||
ns = bool((longdata >> 34) & 1)
|
||||
ew_vel = (longdata >> 35) & 0x7FF - 1
|
||||
ew = bool((longdata >> 46) & 1)
|
||||
subtype = (longdata >> 48) & 0x07
|
||||
|
||||
velocity = math.hypot(ns_vel, ew_vel)
|
||||
if ew:
|
||||
ew_vel = 0 - ew_vel
|
||||
if ns:
|
||||
ns_vel = 0 - ns_vel
|
||||
heading = math.atan2(ew_vel, ns_vel) * (180.0 / math.pi)
|
||||
if heading < 0:
|
||||
heading += 360
|
||||
|
||||
#retstr = "Type 17 subtype 09-0 (track report) from " + "%x" % icao24 + " with velocity " + "%.0f" % velocity + "kt heading " + "%.0f" % heading + " VS " + "%.0f" % vert_spd
|
||||
|
||||
return [velocity, heading, vert_spd]
|
||||
|
||||
def parseBDS09_1(self, shortdata, longdata, parity, ecc):
|
||||
icao24 = shortdata & 0xFFFFFF
|
||||
alt_geo_diff = longdata & 0x7F - 1
|
||||
above_below = bool((longdata >> 7) & 1)
|
||||
if above_below:
|
||||
alt_geo_diff = 0 - alt_geo_diff;
|
||||
vert_spd = float((longdata >> 10) & 0x1FF - 1)
|
||||
ud = bool((longdata >> 19) & 1)
|
||||
if ud:
|
||||
vert_spd = 0 - vert_spd
|
||||
vert_src = bool((longdata >> 20) & 1)
|
||||
ns_vel = float((longdata >> 21) & 0x3FF - 1)
|
||||
ns = bool((longdata >> 31) & 1)
|
||||
ew_vel = float((longdata >> 32) & 0x3FF - 1)
|
||||
ew = bool((longdata >> 42) & 1)
|
||||
subtype = (longdata >> 48) & 0x07
|
||||
|
||||
|
||||
if subtype == 0x02:
|
||||
ns_vel *= 4
|
||||
ew_vel *= 4
|
||||
|
||||
|
||||
vert_spd *= 64
|
||||
alt_geo_diff *= 25
|
||||
|
||||
velocity = math.hypot(ns_vel, ew_vel)
|
||||
if ew:
|
||||
ew_vel = 0 - ew_vel
|
||||
|
||||
if ns_vel == 0:
|
||||
heading = 0
|
||||
else:
|
||||
heading = math.atan(float(ew_vel) / float(ns_vel)) * (180.0 / math.pi)
|
||||
if ns:
|
||||
heading = 180 - heading
|
||||
if heading < 0:
|
||||
heading += 360
|
||||
|
||||
#retstr = "Type 17 subtype 09-1 (track report) from " + "%x" % icao24 + " with velocity " + "%.0f" % velocity + "kt heading " + "%.0f" % heading + " VS " + "%.0f" % vert_spd
|
||||
|
||||
return [velocity, heading, vert_spd]
|
||||
164
python/modes_print.py
Normal file
164
python/modes_print.py
Normal file
@@ -0,0 +1,164 @@
|
||||
#
|
||||
# Copyright 2010 Nick Foster
|
||||
#
|
||||
# This file is part of gr-air-modes
|
||||
#
|
||||
# gr-air-modes is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation; either version 3, or (at your option)
|
||||
# any later version.
|
||||
#
|
||||
# gr-air-modes is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with gr-air-modes; see the file COPYING. If not, write to
|
||||
# the Free Software Foundation, Inc., 51 Franklin Street,
|
||||
# Boston, MA 02110-1301, USA.
|
||||
#
|
||||
|
||||
|
||||
import time, os, sys
|
||||
from string import split, join
|
||||
import modes_parse
|
||||
import math
|
||||
|
||||
class modes_output_print(modes_parse.modes_parse):
|
||||
def __init__(self, mypos):
|
||||
modes_parse.modes_parse.__init__(self, mypos)
|
||||
|
||||
def parse(self, message):
|
||||
[msgtype, shortdata, longdata, parity, ecc, reference, timestamp] = message.split()
|
||||
shortdata = long(shortdata, 16)
|
||||
longdata = long(longdata, 16)
|
||||
parity = long(parity, 16)
|
||||
ecc = long(ecc, 16)
|
||||
reference = float(reference)
|
||||
timestamp = float(timestamp)
|
||||
|
||||
msgtype = int(msgtype)
|
||||
|
||||
output = None;
|
||||
|
||||
if msgtype == 0:
|
||||
output = self.print0(shortdata, parity, ecc)
|
||||
elif msgtype == 4:
|
||||
output = self.print4(shortdata, parity, ecc)
|
||||
elif msgtype == 5:
|
||||
output = self.print5(shortdata, parity, ecc)
|
||||
elif msgtype == 11:
|
||||
output = self.print11(shortdata, parity, ecc)
|
||||
elif msgtype == 17:
|
||||
output = self.print17(shortdata, longdata, parity, ecc)
|
||||
else:
|
||||
output = "No handler for message type " + str(msgtype) + " from %x" % ecc
|
||||
|
||||
if reference == 0.0:
|
||||
refdb = -150.0
|
||||
else:
|
||||
refdb = 10.0*math.log10(reference)
|
||||
|
||||
if output is not None:
|
||||
output = "(%.0f %.10f) " % (refdb, timestamp) + output
|
||||
print output
|
||||
|
||||
def print0(self, shortdata, parity, ecc):
|
||||
[vs, cc, sl, ri, altitude] = self.parse0(shortdata, parity, ecc)
|
||||
|
||||
retstr = "Type 0 (short A-A surveillance) from " + "%x" % ecc + " at " + str(altitude) + "ft"
|
||||
# the ri values below 9 are used for other things. might want to print those someday.
|
||||
if ri == 9:
|
||||
retstr = retstr + " (speed <75kt)"
|
||||
elif ri > 9:
|
||||
retstr = retstr + " (speed " + str(75 * (1 << (ri-10))) + "-" + str(75 * (1 << (ri-9))) + "kt)"
|
||||
|
||||
if vs is True:
|
||||
retstr = retstr + " (aircraft is on the ground)"
|
||||
|
||||
return retstr
|
||||
|
||||
def print4(self, shortdata, parity, ecc):
|
||||
|
||||
[fs, dr, um, altitude] = self.parse4(shortdata, parity, ecc)
|
||||
|
||||
retstr = "Type 4 (short surveillance altitude reply) from " + "%x" % ecc + " at " + str(altitude) + "ft"
|
||||
|
||||
if fs == 1:
|
||||
retstr = retstr + " (aircraft is on the ground)"
|
||||
elif fs == 2:
|
||||
retstr = retstr + " (AIRBORNE ALERT)"
|
||||
elif fs == 3:
|
||||
retstr = retstr + " (GROUND ALERT)"
|
||||
elif fs == 4:
|
||||
retstr = retstr + " (SPI ALERT)"
|
||||
elif fs == 5:
|
||||
retstr = retstr + " (SPI)"
|
||||
|
||||
return retstr
|
||||
|
||||
def print5(self, shortdata, parity, ecc):
|
||||
[fs, dr, um] = self.parse5(shortdata, parity, ecc)
|
||||
|
||||
retstr = "Type 5 (short surveillance ident reply) from " + "%x" % ecc + " with ident " + str(shortdata & 0x1FFF)
|
||||
|
||||
if fs == 1:
|
||||
retstr = retstr + " (aircraft is on the ground)"
|
||||
elif fs == 2:
|
||||
retstr = retstr + " (AIRBORNE ALERT)"
|
||||
elif fs == 3:
|
||||
retstr = retstr + " (GROUND ALERT)"
|
||||
elif fs == 4:
|
||||
retstr = retstr + " (SPI ALERT)"
|
||||
elif fs == 5:
|
||||
retstr = retstr + " (SPI)"
|
||||
|
||||
return retstr
|
||||
|
||||
def print11(self, shortdata, parity, ecc):
|
||||
[icao24, interrogator, ca] = self.parse11(shortdata, parity, ecc)
|
||||
|
||||
retstr = "Type 11 (all call reply) from " + "%x" % icao24 + " in reply to interrogator " + str(interrogator)
|
||||
return retstr
|
||||
|
||||
def print17(self, shortdata, longdata, parity, ecc):
|
||||
icao24 = shortdata & 0xFFFFFF
|
||||
subtype = (longdata >> 51) & 0x1F;
|
||||
|
||||
retstr = None
|
||||
|
||||
if subtype == 4:
|
||||
msg = self.parseBDS08(shortdata, longdata, parity, ecc)
|
||||
retstr = "Type 17 subtype 04 (ident) from " + "%x" % icao24 + " with data " + msg
|
||||
|
||||
elif subtype >= 5 and subtype <= 8:
|
||||
[altitude, decoded_lat, decoded_lon, rnge, bearing] = self.parseBDS06(shortdata, longdata, parity, ecc)
|
||||
if decoded_lat is not None:
|
||||
retstr = "Type 17 subtype 06 (surface report) from " + "%x" % icao24 + " at (" + "%.6f" % decoded_lat + ", " + "%.6f" % decoded_lon + ") (" + "%.2f" % rnge + " @ " + "%.0f" % bearing + ")"
|
||||
|
||||
elif subtype >= 9 and subtype <= 18:
|
||||
[altitude, decoded_lat, decoded_lon, rnge, bearing] = self.parseBDS05(shortdata, longdata, parity, ecc)
|
||||
if decoded_lat is not None:
|
||||
retstr = "Type 17 subtype 05 (position report) from " + "%x" % icao24 + " at (" + "%.6f" % decoded_lat + ", " + "%.6f" % decoded_lon + ") (" + "%.2f" % rnge + " @ " + "%.0f" % bearing + ") at " + str(altitude) + "ft"
|
||||
|
||||
# this is a trigger to capture the bizarre BDS0,5 squitters you keep seeing on the map with latitudes all over the place
|
||||
# if icao24 == 0xa1ede9:
|
||||
# print "Buggy squitter with shortdata %s longdata %s parity %s ecc %s" % (str(shortdata), str(longdata), str(parity), str(ecc),)
|
||||
|
||||
elif subtype == 19:
|
||||
subsubtype = (longdata >> 48) & 0x07
|
||||
if subsubtype == 0:
|
||||
[velocity, heading, vert_spd] = self.parseBDS09_0(shortdata, longdata, parity, ecc)
|
||||
retstr = "Type 17 subtype 09-0 (track report) from " + "%x" % icao24 + " with velocity " + "%.0f" % velocity + "kt heading " + "%.0f" % heading + " VS " + "%.0f" % vert_spd
|
||||
|
||||
elif subsubtype == 1:
|
||||
[velocity, heading, vert_spd] = self.parseBDS09_1(shortdata, longdata, parity, ecc)
|
||||
retstr = "Type 17 subtype 09-1 (track report) from " + "%x" % icao24 + " with velocity " + "%.0f" % velocity + "kt heading " + "%.0f" % heading + " VS " + "%.0f" % vert_spd
|
||||
|
||||
else:
|
||||
retstr = "Type 17 subtype 09-%i" % (subsubtype) + " not implemented"
|
||||
else:
|
||||
retstr = "Type 17 subtype " + str(subtype) + " not implemented"
|
||||
|
||||
return retstr
|
||||
52
python/modes_raw_server.py
Normal file
52
python/modes_raw_server.py
Normal file
@@ -0,0 +1,52 @@
|
||||
#
|
||||
# Copyright 2010 Nick Foster
|
||||
#
|
||||
# This file is part of gr-air-modes
|
||||
#
|
||||
# gr-air-modes is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation; either version 3, or (at your option)
|
||||
# any later version.
|
||||
#
|
||||
# gr-air-modes is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with gr-air-modes; see the file COPYING. If not, write to
|
||||
# the Free Software Foundation, Inc., 51 Franklin Street,
|
||||
# Boston, MA 02110-1301, USA.
|
||||
#
|
||||
|
||||
|
||||
import time, os, sys, socket
|
||||
from string import split, join
|
||||
from datetime import *
|
||||
|
||||
class modes_raw_server:
|
||||
def __init__(self):
|
||||
self._s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
self._s.bind(('', 9988))
|
||||
self._s.listen(1)
|
||||
self._s.setblocking(0) #nonblocking
|
||||
self._conns = [] #list of active connections
|
||||
|
||||
def __del__(self):
|
||||
self._s.close()
|
||||
|
||||
def output(self, msg):
|
||||
for conn in self._conns[:]: #iterate over a copy of the list
|
||||
try:
|
||||
conn.send(msg)
|
||||
except socket.error:
|
||||
self._conns.remove(conn)
|
||||
print "Connections: ", len(self._conns)
|
||||
|
||||
def add_pending_conns(self):
|
||||
try:
|
||||
conn, addr = self._s.accept()
|
||||
self._conns.append(conn)
|
||||
print "Connections: ", len(self._conns)
|
||||
except socket.error:
|
||||
pass
|
||||
202
python/modes_sbs1.py
Normal file
202
python/modes_sbs1.py
Normal file
@@ -0,0 +1,202 @@
|
||||
#
|
||||
# Copyright 2010 Nick Foster
|
||||
#
|
||||
# This file is part of gr-air-modes
|
||||
#
|
||||
# gr-air-modes is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation; either version 3, or (at your option)
|
||||
# any later version.
|
||||
#
|
||||
# gr-air-modes is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with gr-air-modes; see the file COPYING. If not, write to
|
||||
# the Free Software Foundation, Inc., 51 Franklin Street,
|
||||
# Boston, MA 02110-1301, USA.
|
||||
#
|
||||
|
||||
|
||||
import time, os, sys, socket
|
||||
from string import split, join
|
||||
import modes_parse
|
||||
from datetime import *
|
||||
|
||||
class modes_output_sbs1(modes_parse.modes_parse):
|
||||
def __init__(self, mypos):
|
||||
modes_parse.modes_parse.__init__(self, mypos)
|
||||
self._s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
self._s.bind(('', 30003))
|
||||
self._s.listen(1)
|
||||
self._s.setblocking(0) #nonblocking
|
||||
self._conns = [] #list of active connections
|
||||
self._aircraft_id_map = {} # dictionary of icao24 to aircraft IDs
|
||||
self._aircraft_id_count = 0 # Current Aircraft ID count
|
||||
|
||||
def __del__(self):
|
||||
self._s.close()
|
||||
|
||||
def get_aircraft_id(self, icao24):
|
||||
if icao24 in self._aircraft_id_map:
|
||||
return self._aircraft_id_map[icao24]
|
||||
|
||||
# Adding this new ID to the dictionary
|
||||
self._aircraft_id_count += 1
|
||||
self._aircraft_id_map[icao24] = self._aircraft_id_count
|
||||
|
||||
# Checking to see if we need to clean up in the event that the
|
||||
# dictionary is getting too large.
|
||||
if len(self._aircraft_id_map) > 1e4:
|
||||
minimum = ('', self._aircraft_id_count)
|
||||
for pair in self._aircraft_id_map:
|
||||
if pair[1] < minimum[1]:
|
||||
minimum = pair
|
||||
self._aircraft_id_map.pop(minimum[0])
|
||||
|
||||
# Finally return the new pair
|
||||
return self._aircraft_id_count
|
||||
|
||||
def output(self, msg):
|
||||
sbs1_msg = self.parse(msg)
|
||||
if sbs1_msg is not None:
|
||||
for conn in self._conns[:]: #iterate over a copy of the list
|
||||
try:
|
||||
conn.send(sbs1_msg)
|
||||
except socket.error:
|
||||
self._conns.remove(conn)
|
||||
print "Connections: ", len(self._conns)
|
||||
|
||||
def add_pending_conns(self):
|
||||
try:
|
||||
conn, addr = self._s.accept()
|
||||
self._conns.append(conn)
|
||||
print "Connections: ", len(self._conns)
|
||||
except socket.error:
|
||||
pass
|
||||
|
||||
def current_time(self):
|
||||
timenow = datetime.now()
|
||||
return [timenow.strftime("%Y/%m/%d"), timenow.strftime("%H:%M:%S.%f")[0:-3]]
|
||||
|
||||
def decode_fs(self, fs):
|
||||
if fs == 0:
|
||||
return "0,0,0,0"
|
||||
elif fs == 1:
|
||||
return "0,0,0,1"
|
||||
elif fs == 2:
|
||||
return "1,0,0,0"
|
||||
elif fs == 3:
|
||||
return "1,0,0,1"
|
||||
elif fs == 4:
|
||||
return "1,0,0,"
|
||||
elif fs == 5:
|
||||
return "0,0,0,"
|
||||
else:
|
||||
return ",,,"
|
||||
|
||||
def parse(self, message):
|
||||
#assembles a SBS-1-style output string from the received message
|
||||
|
||||
[msgtype, shortdata, longdata, parity, ecc, reference, timestamp] = message.split()
|
||||
|
||||
shortdata = long(shortdata, 16)
|
||||
longdata = long(longdata, 16)
|
||||
parity = long(parity, 16)
|
||||
ecc = long(ecc, 16)
|
||||
msgtype = int(msgtype)
|
||||
outmsg = None
|
||||
|
||||
if msgtype == 0:
|
||||
outmsg = self.pp0(shortdata, parity, ecc)
|
||||
elif msgtype == 4:
|
||||
outmsg = self.pp4(shortdata, parity, ecc)
|
||||
elif msgtype == 5:
|
||||
outmsg = self.pp5(shortdata, parity, ecc)
|
||||
elif msgtype == 11:
|
||||
outmsg = self.pp11(shortdata, parity, ecc)
|
||||
elif msgtype == 17:
|
||||
outmsg = self.pp17(shortdata, longdata, parity, ecc)
|
||||
return outmsg
|
||||
|
||||
def pp0(self, shortdata, parity, ecc):
|
||||
[datestr, timestr] = self.current_time()
|
||||
[vs, cc, sl, ri, altitude] = self.parse0(shortdata, parity, ecc)
|
||||
aircraft_id = self.get_aircraft_id(ecc)
|
||||
retstr = "MSG,7,0,%i,%X,%i,%s,%s,%s,%s,,%s,,,,,,,,,," % (aircraft_id, ecc, aircraft_id+100, datestr, timestr, datestr, timestr, altitude)
|
||||
if vs:
|
||||
retstr += "1\n"
|
||||
else:
|
||||
retstr += "0\n"
|
||||
return retstr
|
||||
|
||||
def pp4(self, shortdata, parity, ecc):
|
||||
[datestr, timestr] = self.current_time()
|
||||
[fs, dr, um, altitude] = self.parse4(shortdata, parity, ecc)
|
||||
aircraft_id = self.get_aircraft_id(ecc)
|
||||
retstr = "MSG,5,0,%i,%X,%i,%s,%s,%s,%s,,%s,,,,,,," % (aircraft_id, ecc, aircraft_id+100, datestr, timestr, datestr, timestr, altitude)
|
||||
return retstr + self.decode_fs(fs) + "\n"
|
||||
|
||||
def pp5(self, shortdata, parity, ecc):
|
||||
# I'm not sure what to do with the identiifcation shortdata & 0x1FFF
|
||||
[datestr, timestr] = self.current_time()
|
||||
[fs, dr, um] = self.parse5(shortdata, parity, ecc)
|
||||
aircraft_id = self.get_aircraft_id(ecc)
|
||||
retstr = "MSG,6,0,%i,%X,%i,%s,%s,%s,%s,,,,,,,,," % (aircraft_id, ecc, aircraft_id+100, datestr, timestr, datestr, timestr)
|
||||
return retstr + self.decode_fs(fs) + "\n"
|
||||
|
||||
def pp11(self, shortdata, parity, ecc):
|
||||
[datestr, timestr] = self.current_time()
|
||||
[icao24, interrogator, ca] = self.parse11(shortdata, parity, ecc)
|
||||
aircraft_id = self.get_aircraft_id(icao24)
|
||||
return "MSG,8,0,%i,%X,%i,%s,%s,%s,%s,,,,,,,,,,,,\n" % (aircraft_id, icao24, aircraft_id+100, datestr, timestr, datestr, timestr)
|
||||
|
||||
def pp17(self, shortdata, longdata, parity, ecc):
|
||||
icao24 = shortdata & 0xFFFFFF
|
||||
aircraft_id = self.get_aircraft_id(icao24)
|
||||
subtype = (longdata >> 51) & 0x1F
|
||||
|
||||
retstr = None
|
||||
#we'll get better timestamps later, hopefully with actual VRT time
|
||||
#in them
|
||||
[datestr, timestr] = self.current_time()
|
||||
|
||||
if subtype >= 1 and subtype <= 4:
|
||||
# Aircraft Identification
|
||||
msg = self.parseBDS08(shortdata, longdata, parity, ecc)
|
||||
retstr = "MSG,1,0,%i,%X,%i,%s,%s,%s,%s,%s,,,,,,,,,,,\n" % (aircraft_id, icao24, aircraft_id+100, datestr, timestr, datestr, timestr, msg)
|
||||
|
||||
elif subtype >= 5 and subtype <= 8:
|
||||
# Surface position measurement
|
||||
[altitude, decoded_lat, decoded_lon, rnge, bearing] = self.parseBDS06(shortdata, longdata, parity, ecc)
|
||||
if decoded_lat is None: #no unambiguously valid position available
|
||||
retstr = None
|
||||
else:
|
||||
retstr = "MSG,2,0,%i,%X,%i,%s,%s,%s,%s,,%i,,,%.5f,%.5f,,,,0,0,0\n" % (aircraft_id, icao24, aircraft_id+100, datestr, timestr, datestr, timestr, altitude, decoded_lat, decoded_lon)
|
||||
|
||||
elif subtype >= 9 and subtype <= 18 and subtype != 15:
|
||||
# Airborne position measurements
|
||||
# WRONG (rnge, bearing), is this still true?
|
||||
# i'm eliminating type 15 records because they don't appear to be
|
||||
# valid position reports.
|
||||
[altitude, decoded_lat, decoded_lon, rnge, bearing] = self.parseBDS05(shortdata, longdata, parity, ecc)
|
||||
if decoded_lat is None: #no unambiguously valid position available
|
||||
retstr = None
|
||||
else:
|
||||
retstr = "MSG,3,0,%i,%X,%i,%s,%s,%s,%s,,%i,,,%.5f,%.5f,,,,0,0,0\n" % (aircraft_id, icao24, aircraft_id+100, datestr, timestr, datestr, timestr, altitude, decoded_lat, decoded_lon)
|
||||
|
||||
elif subtype == 19:
|
||||
# Airborne velocity measurements
|
||||
# WRONG (heading, vert_spd), Is this still true?
|
||||
subsubtype = (longdata >> 48) & 0x07
|
||||
if subsubtype == 0:
|
||||
[velocity, heading, vert_spd] = self.parseBDS09_0(shortdata, longdata, parity, ecc)
|
||||
retstr = "MSG,4,0,%i,%X,%i,%s,%s,%s,%s,,,%.1f,%.1f,,,%i,,,,,\n" % (aircraft_id, icao24, aircraft_id+100, datestr, timestr, datestr, timestr, velocity, heading, vert_spd)
|
||||
|
||||
elif subsubtype == 1:
|
||||
[velocity, heading, vert_spd] = self.parseBDS09_1(shortdata, longdata, parity, ecc)
|
||||
retstr = "MSG,4,0,%i,%X,%i,%s,%s,%s,%s,,,%.1f,%.1f,,,%i,,,,,\n" % (aircraft_id, icao24, aircraft_id+100, datestr, timestr, datestr, timestr, velocity, heading, vert_spd)
|
||||
|
||||
return retstr
|
||||
126
python/modes_sql.py
Normal file
126
python/modes_sql.py
Normal file
@@ -0,0 +1,126 @@
|
||||
#
|
||||
# Copyright 2010 Nick Foster
|
||||
#
|
||||
# This file is part of gr-air-modes
|
||||
#
|
||||
# gr-air-modes is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation; either version 3, or (at your option)
|
||||
# any later version.
|
||||
#
|
||||
# gr-air-modes is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with gr-air-modes; see the file COPYING. If not, write to
|
||||
# the Free Software Foundation, Inc., 51 Franklin Street,
|
||||
# Boston, MA 02110-1301, USA.
|
||||
#
|
||||
|
||||
import time, os, sys
|
||||
from string import split, join
|
||||
import modes_parse
|
||||
import sqlite3
|
||||
|
||||
class modes_output_sql(modes_parse.modes_parse):
|
||||
def __init__(self, mypos, filename):
|
||||
modes_parse.modes_parse.__init__(self, mypos)
|
||||
#create the database
|
||||
self.db = sqlite3.connect(filename)
|
||||
#now execute a schema to create the tables you need
|
||||
c = self.db.cursor()
|
||||
query = """CREATE TABLE IF NOT EXISTS "positions" (
|
||||
"icao" INTEGER KEY NOT NULL,
|
||||
"seen" TEXT NOT NULL,
|
||||
"alt" INTEGER,
|
||||
"lat" REAL,
|
||||
"lon" REAL
|
||||
);"""
|
||||
c.execute(query)
|
||||
query = """CREATE TABLE IF NOT EXISTS "vectors" (
|
||||
"icao" INTEGER KEY NOT NULL,
|
||||
"seen" TEXT NOT NULL,
|
||||
"speed" REAL,
|
||||
"heading" REAL,
|
||||
"vertical" REAL
|
||||
);"""
|
||||
c.execute(query)
|
||||
query = """CREATE TABLE IF NOT EXISTS "ident" (
|
||||
"icao" INTEGER PRIMARY KEY NOT NULL,
|
||||
"ident" TEXT NOT NULL
|
||||
);"""
|
||||
c.execute(query)
|
||||
c.close()
|
||||
|
||||
def __del__(self):
|
||||
self.db.close()
|
||||
|
||||
def insert(self, message):
|
||||
query = self.make_insert_query(message)
|
||||
if query is not None:
|
||||
c = self.db.cursor()
|
||||
c.execute(query)
|
||||
c.close()
|
||||
self.db.commit() #not sure if i have to do this
|
||||
|
||||
def make_insert_query(self, message):
|
||||
#assembles a SQL query tailored to our database
|
||||
#this version ignores anything that isn't Type 17 for now, because we just don't care
|
||||
[msgtype, shortdata, longdata, parity, ecc, reference, timestamp] = message.split()
|
||||
|
||||
shortdata = long(shortdata, 16)
|
||||
longdata = long(longdata, 16)
|
||||
parity = long(parity, 16)
|
||||
ecc = long(ecc, 16)
|
||||
# reference = float(reference)
|
||||
|
||||
msgtype = int(msgtype)
|
||||
|
||||
query = None
|
||||
|
||||
if msgtype == 17:
|
||||
query = self.sql17(shortdata, longdata, parity, ecc)
|
||||
|
||||
return query
|
||||
|
||||
def sql17(self, shortdata, longdata, parity, ecc):
|
||||
icao24 = shortdata & 0xFFFFFF
|
||||
subtype = (longdata >> 51) & 0x1F
|
||||
|
||||
retstr = None
|
||||
|
||||
if subtype == 4:
|
||||
msg = self.parseBDS08(shortdata, longdata, parity, ecc)
|
||||
retstr = "INSERT OR REPLACE INTO ident (icao, ident) VALUES (" + "%i" % icao24 + ", '" + msg + "')"
|
||||
|
||||
elif subtype >= 5 and subtype <= 8:
|
||||
[altitude, decoded_lat, decoded_lon, rnge, bearing] = self.parseBDS06(shortdata, longdata, parity, ecc)
|
||||
if decoded_lat is None: #no unambiguously valid position available
|
||||
retstr = None
|
||||
else:
|
||||
retstr = "INSERT INTO positions (icao, seen, alt, lat, lon) VALUES (" + "%i" % icao24 + ", datetime('now'), " + str(altitude) + ", " + "%.6f" % decoded_lat + ", " + "%.6f" % decoded_lon + ")"
|
||||
|
||||
elif subtype >= 9 and subtype <= 18 and subtype != 15: #i'm eliminating type 15 records because they don't appear to be valid position reports.
|
||||
[altitude, decoded_lat, decoded_lon, rnge, bearing] = self.parseBDS05(shortdata, longdata, parity, ecc)
|
||||
if decoded_lat is None: #no unambiguously valid position available
|
||||
retstr = None
|
||||
else:
|
||||
retstr = "INSERT INTO positions (icao, seen, alt, lat, lon) VALUES (" + "%i" % icao24 + ", datetime('now'), " + str(altitude) + ", " + "%.6f" % decoded_lat + ", " + "%.6f" % decoded_lon + ")"
|
||||
|
||||
elif subtype == 19:
|
||||
subsubtype = (longdata >> 48) & 0x07
|
||||
if subsubtype == 0:
|
||||
[velocity, heading, vert_spd] = self.parseBDS09_0(shortdata, longdata, parity, ecc)
|
||||
retstr = "INSERT INTO vectors (icao, seen, speed, heading, vertical) VALUES (" + "%i" % icao24 + ", datetime('now'), " + "%.0f" % velocity + ", " + "%.0f" % heading + ", " + "%.0f" % vert_spd + ")";
|
||||
|
||||
elif subsubtype == 1:
|
||||
[velocity, heading, vert_spd] = self.parseBDS09_1(shortdata, longdata, parity, ecc)
|
||||
retstr = "INSERT INTO vectors (icao, seen, speed, heading, vertical) VALUES (" + "%i" % icao24 + ", datetime('now'), " + "%.0f" % velocity + ", " + "%.0f" % heading + ", " + "%.0f" % vert_spd + ")";
|
||||
|
||||
return retstr
|
||||
|
||||
|
||||
|
||||
|
||||
59
python/qa_gr-air-modes.py
Executable file
59
python/qa_gr-air-modes.py
Executable file
@@ -0,0 +1,59 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# Copyright 2004,2007 Free Software Foundation, Inc.
|
||||
#
|
||||
# This file is part of GNU Radio
|
||||
#
|
||||
# GNU Radio is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation; either version 3, or (at your option)
|
||||
# any later version.
|
||||
#
|
||||
# GNU Radio is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with GNU Radio; see the file COPYING. If not, write to
|
||||
# the Free Software Foundation, Inc., 51 Franklin Street,
|
||||
# Boston, MA 02110-1301, USA.
|
||||
#
|
||||
|
||||
from gnuradio import gr, gr_unittest
|
||||
import gr-air-modes_swig
|
||||
|
||||
class qa_gr-air-modes (gr_unittest.TestCase):
|
||||
|
||||
def setUp (self):
|
||||
self.tb = gr.top_block ()
|
||||
|
||||
def tearDown (self):
|
||||
self.tb = None
|
||||
|
||||
def test_001_square_ff (self):
|
||||
src_data = (-3, 4, -5.5, 2, 3)
|
||||
expected_result = (9, 16, 30.25, 4, 9)
|
||||
src = gr.vector_source_f (src_data)
|
||||
sqr = gr-air-modes_swig.square_ff ()
|
||||
dst = gr.vector_sink_f ()
|
||||
self.tb.connect (src, sqr)
|
||||
self.tb.connect (sqr, dst)
|
||||
self.tb.run ()
|
||||
result_data = dst.data ()
|
||||
self.assertFloatTuplesAlmostEqual (expected_result, result_data, 6)
|
||||
|
||||
def test_002_square2_ff (self):
|
||||
src_data = (-3, 4, -5.5, 2, 3)
|
||||
expected_result = (9, 16, 30.25, 4, 9)
|
||||
src = gr.vector_source_f (src_data)
|
||||
sqr = gr-air-modes_swig.square2_ff ()
|
||||
dst = gr.vector_sink_f ()
|
||||
self.tb.connect (src, sqr)
|
||||
self.tb.connect (sqr, dst)
|
||||
self.tb.run ()
|
||||
result_data = dst.data ()
|
||||
self.assertFloatTuplesAlmostEqual (expected_result, result_data, 6)
|
||||
|
||||
if __name__ == '__main__':
|
||||
gr_unittest.main ()
|
||||
8
python/testparse.py
Normal file
8
python/testparse.py
Normal file
@@ -0,0 +1,8 @@
|
||||
#!/usr/bin/env python
|
||||
import modes_print
|
||||
|
||||
infile = open("27augrudi3.txt")
|
||||
|
||||
printer = modes_print.modes_output_print([37.409348,-122.07732])
|
||||
for line in infile:
|
||||
printer.parse(line)
|
||||
Reference in New Issue
Block a user