From d13e1bd12e69e8d322ec6213216008c43b64d683 Mon Sep 17 00:00:00 2001 From: Junzi Sun Date: Wed, 20 Dec 2017 17:00:02 +0100 Subject: [PATCH] initial commit for v2.0 --- README.rst | 11 +- pyModeS/__init__.py | 10 +- pyModeS/decoder/__init__.py | 1 + pyModeS/{ => decoder}/adsb.py | 0 pyModeS/{ => decoder}/ehs.py | 0 pyModeS/{ => decoder}/els.py | 0 pyModeS/{ => decoder}/modes_common.py | 0 pyModeS/{ => decoder}/util.py | 0 pyModeS/streamer/aero.py | 178 ++++++++++++++++++++++++++ pyModeS/streamer/client.py | 163 +++++++++++++++++++++++ pyModeS/streamer/pmstream.py | 95 ++++++++++++++ pyModeS/streamer/screen.py | 133 +++++++++++++++++++ pyModeS/streamer/stream.py | 169 ++++++++++++++++++++++++ setup.py | 3 +- tox.ini | 2 +- 15 files changed, 756 insertions(+), 9 deletions(-) create mode 100644 pyModeS/decoder/__init__.py rename pyModeS/{ => decoder}/adsb.py (100%) rename pyModeS/{ => decoder}/ehs.py (100%) rename pyModeS/{ => decoder}/els.py (100%) rename pyModeS/{ => decoder}/modes_common.py (100%) rename pyModeS/{ => decoder}/util.py (100%) create mode 100644 pyModeS/streamer/aero.py create mode 100644 pyModeS/streamer/client.py create mode 100644 pyModeS/streamer/pmstream.py create mode 100644 pyModeS/streamer/screen.py create mode 100644 pyModeS/streamer/stream.py diff --git a/README.rst b/README.rst index 2bbb4ce..9e79bbb 100644 --- a/README.rst +++ b/README.rst @@ -1,5 +1,5 @@ -The Python Mode-S Decoder -========================= +The Python Mode-S Decoder (2.0-Dev branch) +========================================== Python library for Mode-S message decoding. Support Downlink Formats (DF) are: @@ -28,6 +28,13 @@ Detailed manual on Mode-S decoding is published by the author, at: http://adsb-decode-guide.readthedocs.io +New features in v2.0 +--------------------- +- ADS-B and EHS data streaming +- Active aircraft viewing (in terminal) +- More advanced BDS identification in Enhanced Mode-S + + Source code ----------- Checkout and contribute to this open source project at: diff --git a/pyModeS/__init__.py b/pyModeS/__init__.py index 3abdc10..fdfb2a5 100644 --- a/pyModeS/__init__.py +++ b/pyModeS/__init__.py @@ -1,6 +1,8 @@ from __future__ import absolute_import, print_function, division -from .util import * -from . import adsb -from . import ehs -from . import els +from .decoder.util import * +from .decoder import adsb +from .decoder import ehs +from .decoder import els +from .decoder import util +from .decoder import modes_common diff --git a/pyModeS/decoder/__init__.py b/pyModeS/decoder/__init__.py new file mode 100644 index 0000000..f298a61 --- /dev/null +++ b/pyModeS/decoder/__init__.py @@ -0,0 +1 @@ +from __future__ import absolute_import, print_function, division diff --git a/pyModeS/adsb.py b/pyModeS/decoder/adsb.py similarity index 100% rename from pyModeS/adsb.py rename to pyModeS/decoder/adsb.py diff --git a/pyModeS/ehs.py b/pyModeS/decoder/ehs.py similarity index 100% rename from pyModeS/ehs.py rename to pyModeS/decoder/ehs.py diff --git a/pyModeS/els.py b/pyModeS/decoder/els.py similarity index 100% rename from pyModeS/els.py rename to pyModeS/decoder/els.py diff --git a/pyModeS/modes_common.py b/pyModeS/decoder/modes_common.py similarity index 100% rename from pyModeS/modes_common.py rename to pyModeS/decoder/modes_common.py diff --git a/pyModeS/util.py b/pyModeS/decoder/util.py similarity index 100% rename from pyModeS/util.py rename to pyModeS/decoder/util.py diff --git a/pyModeS/streamer/aero.py b/pyModeS/streamer/aero.py new file mode 100644 index 0000000..5557813 --- /dev/null +++ b/pyModeS/streamer/aero.py @@ -0,0 +1,178 @@ +""" +Functions for aeronautics in this module + - physical quantities always in SI units + - lat,lon,course and heading in degrees + +International Standard Atmosphere + p,rho,T = atmos(H) # atmos as function of geopotential altitude H [m] + a = vsound(H) # speed of sound [m/s] as function of H[m] + p = pressure(H) # calls atmos but retruns only pressure [Pa] + T = temperature(H) # calculates temperature [K] + rho = density(H) # calls atmos but retruns only pressure [Pa] + +Speed conversion at altitude H[m] in ISA: + Mach = tas2mach(Vtas,H) # true airspeed (Vtas) to mach number conversion + Vtas = mach2tas(Mach,H) # true airspeed (Vtas) to mach number conversion + Vtas = eas2tas(Veas,H) # equivalent airspeed to true airspeed, H in [m] + Veas = tas2eas(Vtas,H) # true airspeed to equivent airspeed, H in [m] + Vtas = cas2tas(Vcas,H) # Vcas to Vtas conversion both m/s, H in [m] + Vcas = tas2cas(Vtas,H) # Vtas to Vcas conversion both m/s, H in [m] + Vcas = mach2cas(Mach,H) # Mach to Vcas conversion Vcas in m/s, H in [m] + Mach = cas2mach(Vcas,H) # Vcas to mach copnversion Vcas in m/s, H in [m] +""" + +import numpy as np + +"""Aero and geo Constants """ +kts = 0.514444 # knot -> m/s +ft = 0.3048 # ft -> m +fpm = 0.00508 # ft/min -> m/s +inch = 0.0254 # inch -> m +sqft = 0.09290304 # 1 square foot +nm = 1852. # nautical mile -> m +lbs = 0.453592 # pound -> kg +g0 = 9.80665 # m/s2, Sea level gravity constant +R = 287.05287 # m2/(s2 x K), gas constant, sea level ISA +p0 = 101325. # Pa, air pressure, sea level ISA +rho0 = 1.225 # kg/m3, air density, sea level ISA +T0 = 288.15 # K, temperature, sea level ISA +gamma = 1.40 # cp/cv for air +gamma1 = 0.2 # (gamma-1)/2 for air +gamma2 = 3.5 # gamma/(gamma-1) for air +beta = -0.0065 # [K/m] ISA temp gradient below tropopause +r_earth = 6371000. # m, average earth radius +a0 = 340.293988 # m/s, sea level speed of sound ISA, sqrt(gamma*R*T0) + + +def atmos(H): + # H in metres + T = np.maximum(288.15 - 0.0065 * H, 216.65) + rhotrop = 1.225 * (T / 288.15)**4.256848030018761 + dhstrat = np.maximum(0., H - 11000.0) + rho = rhotrop * np.exp(-dhstrat / 6341.552161) + p = rho * R * T + return p, rho, T + + +def temperature(H): + p, r, T = atmos(H) + return T + + +def pressure(H): + p, r, T = atmos(H) + return p + + +def density(H): + p, r, T = atmos(H) + return r + + +def vsound(H): + """Speed of sound""" + T = temperature(H) + a = np.sqrt(gamma * R * T) + return a + + +def distance(lat1, lon1, lat2, lon2, H=0): + """ + Compute spherical distance from spherical coordinates. + + For two locations in spherical coordinates + (1, theta, phi) and (1, theta', phi') + cosine( arc length ) = + sin phi sin phi' cos(theta-theta') + cos phi cos phi' + distance = rho * arc length + """ + + # phi = 90 - latitude + phi1 = np.radians(90.0 - lat1) + phi2 = np.radians(90.0 - lat2) + + # theta = longitude + theta1 = np.radians(lon1) + theta2 = np.radians(lon2) + + cos = np.sin(phi1) * np.sin(phi2) * np.cos(theta1 - theta2) \ + + np.cos(phi1) * np.cos(phi2) + + arc = np.arccos(cos) + dist = arc * (r_earth + H) # meters, radius of earth + return dist + + +def bearing(lat1, lon1, lat2, lon2): + lat1 = np.radians(lat1) + lon1 = np.radians(lon1) + lat2 = np.radians(lat2) + lon2 = np.radians(lon2) + x = np.sin(lon2-lon1) * np.cos(lat2) + y = np.cos(lat1) * np.sin(lat2) \ + - np.sin(lat1) * np.cos(lat2) * np.cos(lon2-lon1) + initial_bearing = np.arctan2(x, y) + initial_bearing = np.degrees(initial_bearing) + bearing = (initial_bearing + 360) % 360 + return bearing + + +# ----------------------------------------------------- +# Speed conversions, altitude H all in meters +# ----------------------------------------------------- +def tas2mach(Vtas, H): + """True Airspeed to Mach number""" + a = vsound(H) + Mach = Vtas/a + return Mach + + +def mach2tas(Mach, H): + """Mach number to True Airspeed""" + a = vsound(H) + Vtas = Mach*a + return Vtas + + +def eas2tas(Veas, H): + """Equivalent Airspeed to True Airspeed""" + rho = density(H) + Vtas = Veas * np.sqrt(rho0/rho) + return Vtas + + +def tas2eas(Vtas, H): + """True Airspeed to Equivalent Airspeed""" + rho = density(H) + Veas = Vtas * np.sqrt(rho/rho0) + return Veas + + +def cas2tas(Vcas, H): + """Calibrated Airspeed to True Airspeed""" + p, rho, T = atmos(H) + qdyn = p0*((1.+rho0*Vcas*Vcas/(7.*p0))**3.5-1.) + Vtas = np.sqrt(7.*p/rho*((1.+qdyn/p)**(2./7.)-1.)) + return Vtas + + +def tas2cas(Vtas, H): + """True Airspeed to Calibrated Airspeed""" + p, rho, T = atmos(H) + qdyn = p*((1.+rho*Vtas*Vtas/(7.*p))**3.5-1.) + Vcas = np.sqrt(7.*p0/rho0*((qdyn/p0+1.)**(2./7.)-1.)) + return Vcas + + +def mach2cas(Mach, H): + """Mach number to Calibrated Airspeed""" + Vtas = mach2tas(Mach, H) + Vcas = tas2cas(Vtas, H) + return Vcas + + +def cas2mach(Vcas, H): + """Calibrated Airspeed to Mach number""" + Vtas = cas2tas(Vcas, H) + Mach = tas2mach(Vtas, H) + return Mach diff --git a/pyModeS/streamer/client.py b/pyModeS/streamer/client.py new file mode 100644 index 0000000..370a631 --- /dev/null +++ b/pyModeS/streamer/client.py @@ -0,0 +1,163 @@ +''' +Stream beast raw data from a TCP server, convert to mode-s messages +''' +from __future__ import print_function, division +import sys +import socket +import time +from threading import Thread + +if (sys.version_info > (3, 0)): + PY_VERSION = 3 +else: + PY_VERSION = 2 + +class BaseClient(Thread): + def __init__(self, host, port): + Thread.__init__(self) + self.host = host + self.port = port + self.buffer = [] + + + def connect(self): + while True: + try: + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + s.settimeout(10) # 10 second timeout + s.connect((self.host, self.port)) + print("Server connected - %s:%s" % (self.host, self.port)) + print("collecting ADS-B messages...") + return s + except socket.error as err: + print("Socket connection error: %s. reconnecting..." % err) + time.sleep(3) + + def read_beast_buffer(self): + ''' + "1" : 6 byte MLAT timestamp, 1 byte signal level, + 2 byte Mode-AC + "2" : 6 byte MLAT timestamp, 1 byte signal level, + 7 byte Mode-S short frame + "3" : 6 byte MLAT timestamp, 1 byte signal level, + 14 byte Mode-S long frame + "4" : 6 byte MLAT timestamp, status data, DIP switch + configuration settings (not on Mode-S Beast classic) + : true 0x1a + is 0x1a, and "1", "2" and "3" are 0x31, 0x32 and 0x33 + + timestamp: + wiki.modesbeast.com/Radarcape:Firmware_Versions#The_GPS_timestamp + ''' + + messages_mlat = [] + msg = [] + i = 0 + + # process the buffer until the last divider 0x1a + # then, reset the self.buffer with the remainder + + while i < len(self.buffer): + if (self.buffer[i:i+2] == [0x1a, 0x1a]): + msg.append(0x1a) + i += 1 + elif (i == len(self.buffer) - 1) and (self.buffer[i] == 0x1a): + # special case where the last bit is 0x1a + msg.append(0x1a) + elif self.buffer[i] == 0x1a: + if i == len(self.buffer) - 1: + # special case where the last bit is 0x1a + msg.append(0x1a) + elif len(msg) > 0: + messages_mlat.append(msg) + msg = [] + else: + msg.append(self.buffer[i]) + i += 1 + + # save the reminder for next reading cycle, if not empty + if len(msg) > 0: + reminder = [] + for i, m in enumerate(msg): + if (m == 0x1a) and (i < len(msg)-1): + # rewind 0x1a, except when it is at the last bit + reminder.extend([m, m]) + else: + reminder.append(m) + self.buffer = [0x1a] + msg + else: + self.buffer = [] + + # extract messages + messages = [] + for mm in messages_mlat: + msgtype = mm[0] + # print(''.join('%02X' % i for i in mm)) + + if msgtype == 0x32: + # Mode-S Short Message, 7 byte, 14-len hexstr + msg = ''.join('%02X' % i for i in mm[8:15]) + elif msgtype == 0x33: + # Mode-S Long Message, 14 byte, 28-len hexstr + msg = ''.join('%02X' % i for i in mm[8:22]) + else: + # Other message tupe + continue + + if len(msg) not in [14, 28]: + # incomplete message + continue + + ts = time.time() + + messages.append([msg, ts]) + return messages + + def handle_messages(self, messages): + """re-implement this method to handle the messages""" + for msg, t in messages: + print("%f %s" % (t, msg)) + + def run(self): + sock = self.connect() + + while True: + try: + received = sock.recv(1024) + + if PY_VERSION == 2: + received = [ord(i) for i in received] + + self.buffer.extend(received) + # print(''.join(x.encode('hex') for x in self.buffer)) + + # process self.buffer when it is longer enough + # if len(self.buffer) < 2048: + # continue + # -- Removed!! Cause delay in low data rate scenario -- + + messages = self.read_beast_buffer() + + if not messages: + continue + else: + self.handle_messages(messages) + + time.sleep(0.001) + except Exception as e: + print("Unexpected Error:", e) + + try: + sock = self.connect() + except Exception as e: + print("Unexpected Error:", e) + + + +if __name__ == '__main__': + # for testing purpose only + host = sys.argv[1] + port = int(sys.argv[2]) + client = BaseClient(host=host, port=port) + client.daemon = True + client.run() diff --git a/pyModeS/streamer/pmstream.py b/pyModeS/streamer/pmstream.py new file mode 100644 index 0000000..15f859a --- /dev/null +++ b/pyModeS/streamer/pmstream.py @@ -0,0 +1,95 @@ +from __future__ import print_function, division +import os +import sys +import curses +import numpy as np +import time +import pyModeS as pms +from threading import Lock +from client import BaseClient +from stream import Stream +from screen import Screen + +LOCK = Lock() +ADSB_MSG = [] +ADSB_TS = [] +EHS_MSG = [] +EHS_TS = [] + +HOST = 'sil.lr.tudelft.nl' +PORT = 30334 + +LAT0 = 51.9899 +LON0 = 4.3754 + +class ModesClient(BaseClient): + def __init__(self, host, port): + super(ModesClient, self).__init__(host, port) + + def handle_messages(self, messages): + local_buffer_adsb_msg = [] + local_buffer_adsb_ts = [] + local_buffer_ehs_msg = [] + local_buffer_ehs_ts = [] + + for msg, t in messages: + if len(msg) < 28: # only process long messages + continue + + df = pms.df(msg) + + if df == 17 or df == 18: + local_buffer_adsb_msg.append(msg) + local_buffer_adsb_ts.append(t) + elif df == 20 or df == 21: + local_buffer_ehs_msg.append(msg) + local_buffer_ehs_ts.append(t) + else: + continue + + + LOCK.acquire() + ADSB_MSG.extend(local_buffer_adsb_msg) + ADSB_TS.extend(local_buffer_adsb_ts) + EHS_MSG.extend(local_buffer_ehs_msg) + EHS_TS.extend(local_buffer_ehs_ts) + LOCK.release() + + +sys.stdout = open(os.devnull, 'w') + +client = ModesClient(host=HOST, port=PORT) +client.daemon = True +client.start() + +stream = Stream(lat0=LAT0, lon0=LON0) + +try: + screen = Screen() + screen.daemon = True + screen.start() + + while True: + if len(ADSB_MSG) > 200: + LOCK.acquire() + stream.process_raw(ADSB_TS, ADSB_MSG, EHS_TS, EHS_MSG) + ADSB_MSG = [] + ADSB_TS = [] + EHS_MSG = [] + EHS_TS = [] + LOCK.release() + + acs = stream.get_aircraft() + # try: + screen.update_data(acs) + screen.update() + # except KeyboardInterrupt: + # raise + # except: + # continue + +except KeyboardInterrupt: + sys.exit(0) + +finally: + curses.endwin() diff --git a/pyModeS/streamer/screen.py b/pyModeS/streamer/screen.py new file mode 100644 index 0000000..cc45e38 --- /dev/null +++ b/pyModeS/streamer/screen.py @@ -0,0 +1,133 @@ +from __future__ import print_function, division +import curses +import numpy as np +import time +from threading import Thread + +COLUMNS = ['lat', 'lon', 'alt', 'gs', 'tas', 'ias', 'mach', 'roc', 'trk', 'hdg', 't'] + +class Screen(Thread): + def __init__(self): + Thread.__init__(self) + self.screen = curses.initscr() + curses.noecho() + self.screen.keypad(True) + self.y = 3 + self.x = 1 + self.offset = 0 + self.acs = {} + self.lock_icao = None + + + def reset_cursor_pos(self): + self.screen.move(self.y, self.x) + + def update_data(self, acs): + self.acs = acs + + def draw_frame(self): + self.screen.border(0) + self.screen.addstr(0, 2, "Online aircraft ('crtl+c' to exit, 'enter' to select)") + + def update(self): + if len(self.acs) == 0: + return + + resized = curses.is_term_resized(self.scr_h, self.scr_w) + if resized is True: + self.scr_h, self.scr_w = self.screen.getmaxyx() + self.screen.clear() + curses.resizeterm(self.scr_h, self.scr_w) + + self.screen.refresh() + self.draw_frame() + + row = 1 + + header = 'icao' + for c in COLUMNS: + c = 'updated' if c=='t' else c + header += '%10s' % c + + if len(header) > self.scr_w - 2: + header = header[:self.scr_w-3] + '>' + + self.screen.addstr(row, 1, header) + + row +=1 + self.screen.addstr(row, 1, '-'*(self.scr_w-2)) + + icaos = np.array(list(self.acs.keys())) + icaos = np.sort(icaos) + + for row in range(3, self.scr_h - 3): + idx = row + self.offset + + if idx > len(icaos) - 1: + line = ' '*(self.scr_w-2) + + else: + line = '' + + icao = icaos[idx] + ac = self.acs[icao] + + line += icao + + for c in COLUMNS: + + if c == 't': + val = str(int(ac[c])) + line += '%12s' % val + else: + val = '' if ac[c] is None else ac[c] + line += '%10s' % val + + if len(line) > self.scr_w - 2: + line = line[:self.scr_w-3] + '>' + + if self.lock_icao == icao: + self.screen.addstr(row, 1, line, curses.A_STANDOUT) + elif row == self.y: + self.screen.addstr(row, 1, line, curses.A_BOLD) + else: + self.screen.addstr(row, 1, line) + + self.screen.addstr(self.scr_h-3, 1, '-'*(self.scr_w-2)) + + total_page = len(icaos) // (self.scr_h - 4) + 1 + current_page = self.offset // (self.scr_h - 4) + 1 + self.screen.addstr(self.scr_h-2, 1, '(%d / %d)' % (current_page, total_page)) + + self.reset_cursor_pos() + + def run(self): + self.draw_frame() + self.scr_h, self.scr_w = self.screen.getmaxyx() + + while True: + c = self.screen.getch() + + if c == curses.KEY_HOME: + self.x = 1 + self.y = 1 + elif c == curses.KEY_NPAGE: + offset_intent = self.offset + (self.scr_h - 4) + if offset_intent < len(self.acs) - 5: + self.offset = offset_intent + elif c == curses.KEY_PPAGE: + offset_intent = self.offset - (self.scr_h - 4) + if offset_intent > 0: + self.offset = offset_intent + else: + self.offset = 0 + elif c == curses.KEY_DOWN : + y_intent = self.y + 1 + if y_intent < self.scr_h - 3: + self.y = y_intent + elif c == curses.KEY_UP: + y_intent = self.y - 1 + if y_intent > 2: + self.y = y_intent + elif c == curses.KEY_ENTER or c == 10 or c == 13: + self.lock_icao = (self.screen.instr(self.y, 1, 6)).decode() diff --git a/pyModeS/streamer/stream.py b/pyModeS/streamer/stream.py new file mode 100644 index 0000000..c2c62c2 --- /dev/null +++ b/pyModeS/streamer/stream.py @@ -0,0 +1,169 @@ +from __future__ import print_function, division +import numpy as np +import time +import pyModeS as pms + + +class Stream(): + def __init__(self, lat0, lon0): + + self.acs = dict() + + self.cache_new_acs = False + self.__new_acs = set() + + self.lat0 = lat0 + self.lon0 = lon0 + + self.t = 0 + self.cache_timeout = 60 # seconds + + + def process_raw(self, adsb_ts, adsb_msgs, ehs_ts, ehs_msgs, tnow=None): + """process a chunk of adsb and ehs messages recieved in the same + time period. + """ + if tnow is None: + tnow = time.time() + + self.t = tnow + + local_updated_acs_buffer = [] + + # process adsb message + for t, msg in zip(adsb_ts, adsb_msgs): + icao = pms.adsb.icao(msg) + tc = pms.adsb.typecode(msg) + + if icao not in self.acs: + self.acs[icao] = { + 'lat': None, + 'lon': None, + 'alt': None, + 'gs': None, + 'trk': None, + 'roc': None, + 'tas': None, + 'ias': None, + 'mach': None, + 'hdg': None, + } + + self.acs[icao]['t'] = t + + if 1 <= tc <= 4: + self.acs[icao]['callsign'] = pms.adsb.callsign(msg) + + if (5 <= tc <= 8) or (tc == 19): + vdata = pms.adsb.velocity(msg) + if vdata is None: + continue + + spd, trk, roc, tag = vdata + if tag != 'GS': + continue + + self.acs[icao]['gs'] = spd + self.acs[icao]['trk'] = trk + self.acs[icao]['roc'] = roc + self.acs[icao]['tv'] = t + + if (5 <= tc <= 18): + oe = pms.adsb.oe_flag(msg) + self.acs[icao][oe] = msg + self.acs[icao]['t'+str(oe)] = t + + if ('tpos' in self.acs[icao]) and (t - self.acs[icao]['tpos'] < 180): + # use single message decoding + rlat = self.acs[icao]['lat'] + rlon = self.acs[icao]['lon'] + latlon = pms.adsb.position_with_ref(msg, rlat, rlon) + elif ('t0' in self.acs[icao]) and ('t1' in self.acs[icao]) and \ + (abs(self.acs[icao]['t0'] - self.acs[icao]['t1']) < 10): + # use multi message decoding + try: + latlon = pms.adsb.position( + self.acs[icao][0], + self.acs[icao][1], + self.acs[icao]['t0'], + self.acs[icao]['t1'], + self.lat0, self.lon0 + ) + except: + # mix of surface and airborne position message + continue + else: + latlon = None + + if latlon is not None: + self.acs[icao]['tpos'] = t + self.acs[icao]['lat'] = latlon[0] + self.acs[icao]['lon'] = latlon[1] + self.acs[icao]['alt'] = pms.adsb.altitude(msg) + local_updated_acs_buffer.append(icao) + + # process ehs message + for t, msg in zip(ehs_ts, ehs_msgs): + icao = pms.ehs.icao(msg) + + if icao not in self.acs: + continue + + bds = pms.ehs.BDS(msg) + + if bds == 'BDS50': + tas = pms.ehs.tas50(msg) + + if tas: + self.acs[icao]['t50'] = t + self.acs[icao]['tas'] = tas + + elif bds == 'BDS60': + ias = pms.ehs.ias60(msg) + hdg = pms.ehs.hdg60(msg) + mach = pms.ehs.mach60(msg) + + if ias or hdg or mach: + self.acs[icao]['t60'] = t + if ias: + self.acs[icao]['ias'] = ias + if hdg: + self.acs[icao]['hdg'] = hdg + if mach: + self.acs[icao]['mach'] = mach + + # clear up old data + for icao in list(self.acs.keys()): + if self.t - self.acs[icao]['t'] > self.cache_timeout: + del self.acs[icao] + continue + + if self.cache_new_acs: + self.add_new_aircraft(local_updated_acs_buffer) + + return + + def get_aircraft(self): + """all aircraft that are stored in memeory""" + acs = self.acs + icaos = list(acs.keys()) + for icao in icaos: + if acs[icao]['lat'] is None: + acs.pop(icao) + return acs + + def add_new_aircraft(self, acs): + """add new aircraft to the list""" + self.__new_acs.update(acs) + return + + def get_new_aircraft(self): + """update aircraft from last iteration""" + newacs = dict() + for ac in self.__new_acs: + newacs[ac] = self.acs[ac] + return newacs + + def reset_new_aircraft(self): + """reset the updated icao buffer once been read""" + self.__new_acs = set() diff --git a/setup.py b/setup.py index f739005..08a5025 100644 --- a/setup.py +++ b/setup.py @@ -30,7 +30,7 @@ setup( # Versions should comply with PEP440. For a discussion on single-sourcing # the version across setup.py and the project code, see # https://packaging.python.org/en/latest/single_source_version.html - version='1.2.0', + version='2.0-dev', description='Python Mode-S Decoder', long_description=long_description, @@ -63,7 +63,6 @@ setup( # Specify the Python versions you support here. In particular, ensure # that you indicate whether you support Python 2, Python 3 or both. 'Programming Language :: Python :: 2', - 'Programming Language :: Python :: 2.6', 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3.3', diff --git a/tox.ini b/tox.ini index 8ab7582..346bc73 100644 --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,6 @@ [tox] toxworkdir=/tmp/tox -envlist = py26,py27,py35 +envlist = py27,py35 [testenv] deps=pytest commands=py.test