CMakeified almost everything. Test code in python/ and apps other than uhd_modes.py still need minor updating.

This commit is contained in:
Nick Foster
2011-12-14 10:17:16 -08:00
parent 4fcf7a4498
commit 8522bc0b25
135 changed files with 17944 additions and 13516 deletions

53
python/CMakeLists.txt Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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

View 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
View 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
View 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
View 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
View 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)