diff --git a/.idea/.name b/.idea/.name
new file mode 100644
index 0000000..da5a910
--- /dev/null
+++ b/.idea/.name
@@ -0,0 +1 @@
+spots
\ No newline at end of file
diff --git a/.idea/cssxfire.xml b/.idea/cssxfire.xml
new file mode 100644
index 0000000..61699e7
--- /dev/null
+++ b/.idea/cssxfire.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/deployment.xml b/.idea/deployment.xml
new file mode 100644
index 0000000..222065d
--- /dev/null
+++ b/.idea/deployment.xml
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/dictionaries/mm.xml b/.idea/dictionaries/mm.xml
new file mode 100644
index 0000000..3aabd29
--- /dev/null
+++ b/.idea/dictionaries/mm.xml
@@ -0,0 +1,12 @@
+
+
+
+ adsb
+ asctime
+ icao
+ levelname
+ squitter
+ sqwk
+
+
+
\ No newline at end of file
diff --git a/.idea/encodings.xml b/.idea/encodings.xml
new file mode 100644
index 0000000..97626ba
--- /dev/null
+++ b/.idea/encodings.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml
new file mode 100644
index 0000000..8086a31
--- /dev/null
+++ b/.idea/inspectionProfiles/Project_Default.xml
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/misc.xml b/.idea/misc.xml
new file mode 100644
index 0000000..550961c
--- /dev/null
+++ b/.idea/misc.xml
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ Bash
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/modules.xml b/.idea/modules.xml
new file mode 100644
index 0000000..e4c7356
--- /dev/null
+++ b/.idea/modules.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/spots.iml b/.idea/spots.iml
new file mode 100644
index 0000000..4563498
--- /dev/null
+++ b/.idea/spots.iml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/vagrant.xml b/.idea/vagrant.xml
new file mode 100644
index 0000000..a5aa786
--- /dev/null
+++ b/.idea/vagrant.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
new file mode 100644
index 0000000..4788a8a
--- /dev/null
+++ b/.idea/vcs.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/README.md b/README.md
index 731c1b8..e56b2c9 100644
--- a/README.md
+++ b/README.md
@@ -29,9 +29,10 @@ The following message will be decoded:
* Comm BDS identity reply (Downlink format: 21)
Messages decoded are displayed either in a serialised format on standard output
-or in a tabular format depending on preference.
+or in a tabular format depending on preference. An inbuilt server is listening on port 5051 (configurable) and
+enables a client to access decoded messages in json format.
-Some statistics is collected.
+Some statistics is collected, this data is also accessible through the server
## Dependencies
@@ -98,6 +99,7 @@ Configuration for spots is in `spots_config.json`. Follows json syntax with no e
* verbose logging (true/false): writes messages to spots logfile
* check crc (true/false): whether to check crc (recommended) or not
+* check phase (true/false): simple check if there is a phase shift and correction
* use metric (true/false): show values in metric system or not (altitude and velocity)
* apply bit error correction (true/false): whether to try to correct bit errors or not (CPU demanding if true)
* read from file (true/false): if true, read samples from a file rather than from the USB dongle
@@ -109,14 +111,40 @@ Configuration for spots is in `spots_config.json`. Follows json syntax with no e
* log file (string): The name of the log file
* log max bytes (integer): How many bytes to log before the log file is rotated
* log backup count (integer): How many roted log files to keep
+* spots server address (localhost or ip-address): the address for the server
+* spots server port (5051): the server port
+
+## Client/Server
+
+Use nginx as proxy server with the following added to the nginx conf file
+
+ location /spots {
+ try_files $uri $uri/ $uri/index.html $uri.html @spots;
+ }
+
+ location @spots {
+ proxy_pass http://rpi2.local:8080;
+ proxy_redirect off;
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Host $server_name;
+ }
+
+So, nginx will forward any http requests to spots (e.g. `http://www.viltstigen.se/spots`) to
+`http://rpi2.local:8080` (spots runs on rpi2-node).
+
+Flask is running using Gunicorn, listening on port 8080, see `emitter.py` and `spots_emitter.conf` for details.
+Use `supervisor` to control processes running as daemons.
+The flask application communicates with the radar application (that listen on port 5051) through a simple text
+protocol, see files `emitter.py` and `server.py`
+
+Using some html, bootstrap css and javascripts (jQuery and Highcharts), see files spots.html and spots.js, it is
+possible to get this view in a web browser.
+
+![preamble](spots.png)
## What's next?
There is probably inconsistencies, bugs, optimizations, documentation etc etc to make.
If you find something, let me know but be aware that this is a leisure thing for me.
-
-Current directions are:
-
-* decode more information from received messages
-* do some more statistical collection
-* add web server/client possibilities
\ No newline at end of file
diff --git a/basic.py b/basic.py
index d929cb4..ee29a85 100644
--- a/basic.py
+++ b/basic.py
@@ -1,5 +1,8 @@
import threading
import json
+import time
+
+__author__ = 'Wolfrax'
"""
basic implement fundamentals of spots, the class ADSB includes constants, pre-defined tables and configuration
@@ -35,7 +38,7 @@ class ADSB:
This class defines fundamental constants and is not supposed to be instantiated
"""
- VERSION = "1.0"
+ VERSION = "2.0"
# basic constants
MODES_SIGMIN = 0
@@ -67,39 +70,39 @@ class ADSB:
KPH_PER_KNOT = 1.852
# Downlink formats
- DF_SHORT_AIR2AIR_SURVEILLANCE_0 = 0
- DF_UNKNOWN_1 = 1
- DF_UNKNOWN_2 = 2
- DF_UNKNOWN_3 = 3
- DF_SURVEILLANCE_ALTITUDE_REPLY_4 = 4
- DF_SURVEILLANCE_IDENTITY_REPLY_5 = 5
- DF_UNKNOWN_6 = 6
- DF_UNKNOWN_7 = 7
- DF_UNKNOWN_8 = 8
- DF_UNKNOWN_9 = 9
- DF_UNKNOWN_10 = 10
- DF_ALL_CALL_REPLY_11 = 11
- DF_UNKNOWN_12 = 12
- DF_UNKNOWN_13 = 13
- DF_UNKNOWN_14 = 14
- DF_UNKNOWN_15 = 15
- DF_LONG_AIR2AIR_SURVEILLANCE_16 = 16
- DF_ADSB_MSG_17 = 17
- DF_EXTENDED_SQUITTER_18 = 18
- DF_MILITARY_EXTENDED_SQUITTER_19 = 19
- DF_COMM_BDS_ALTITUDE_REPLY_20 = 20
- DF_COMM_BDS_IDENTITY_REPLY_21 = 21
- DF_MILITARY_USE_22 = 22
- DF_UNKNOWN_23 = 23
- DF_COMM_D_EXTENDED_LENGTH_MESSAGE_24 = 24
- DF_UNKNOWN_25 = 25
- DF_UNKNOWN_26 = 26
- DF_UNKNOWN_27 = 27
- DF_UNKNOWN_28 = 28
- DF_UNKNOWN_29 = 29
- DF_UNKNOWN_30 = 30
- DF_UNKNOWN_31 = 31
- DF_SSR_MODE_AC_REPLY_32 = 32
+ DF_SHORT_AIR2AIR_SURVEILLANCE_0 = "0"
+ DF_UNKNOWN_1 = "1"
+ DF_UNKNOWN_2 = "2"
+ DF_UNKNOWN_3 = "3"
+ DF_SURVEILLANCE_ALTITUDE_REPLY_4 = "4"
+ DF_SURVEILLANCE_IDENTITY_REPLY_5 = "5"
+ DF_UNKNOWN_6 = "6"
+ DF_UNKNOWN_7 = "7"
+ DF_UNKNOWN_8 = "8"
+ DF_UNKNOWN_9 = "9"
+ DF_UNKNOWN_10 = "10"
+ DF_ALL_CALL_REPLY_11 = "11"
+ DF_UNKNOWN_12 = "12"
+ DF_UNKNOWN_13 = "13"
+ DF_UNKNOWN_14 = "14"
+ DF_UNKNOWN_15 = "15"
+ DF_LONG_AIR2AIR_SURVEILLANCE_16 = "16"
+ DF_ADSB_MSG_17 = "17"
+ DF_EXTENDED_SQUITTER_18 = "18"
+ DF_MILITARY_EXTENDED_SQUITTER_19 = "19"
+ DF_COMM_BDS_ALTITUDE_REPLY_20 = "20"
+ DF_COMM_BDS_IDENTITY_REPLY_21 = "21"
+ DF_MILITARY_USE_22 = "22"
+ DF_UNKNOWN_23 = "23"
+ DF_COMM_D_EXTENDED_LENGTH_MESSAGE_24 = "24"
+ DF_UNKNOWN_25 = "25"
+ DF_UNKNOWN_26 = "26"
+ DF_UNKNOWN_27 = "27"
+ DF_UNKNOWN_28 = "28"
+ DF_UNKNOWN_29 = "29"
+ DF_UNKNOWN_30 = "30"
+ DF_UNKNOWN_31 = "31"
+ DF_SSR_MODE_AC_REPLY_32 = "32"
# Type codes
TC_NO_INFO_0 = 0
@@ -170,6 +173,7 @@ class ADSB:
cfg_check_phase = config["check phase"]
cfg_use_metric = config["use metric"]
cfg_apply_bit_err_correction = config["apply bit err correction"]
+ cfg_run_as_daemon = config["run as daemon"]
cfg_read_from_file = config["read from file"]
cfg_file_name = config["file name"]
cfg_use_text_display = config["use text display"]
@@ -181,6 +185,8 @@ class ADSB:
cfg_log_file = config["log file"]
cfg_log_max_bytes = config["log max bytes"]
cfg_log_backup_count = config["log backup count"]
+ cfg_server_address = config["spots server address"]
+ cfg_server_port = config["spots server port"]
def __init__(self):
pass
@@ -561,6 +567,7 @@ def __init__(self, interval, func, name):
self.interval = interval
self.function = func
self.finished = threading.Event()
+ self.daemon = True
def run(self):
while not self.finished.is_set():
@@ -576,35 +583,52 @@ class Stats:
"""
Class for collecting some statistics on messages
"""
- valid_preambles = 0
- valid_crc = 0
- not_valid_crc = 0
- df_0 = 0
- df_4 = 0
- df_5 = 0
- df_16 = 0
- df_17 = 0
- df_18 = 0
- df_20 = 0
- df_21 = 0
+ data = {'spots_version': "",
+ 'start_time': 0,
+ 'start_time_string': "",
+ 'valid_preambles': 0,
+ 'valid_crc': 0,
+ 'not_valid_crc': 0,
+ 'df_0': 0,
+ 'df_4': 0,
+ 'df_5': 0,
+ 'df_11': 0,
+ 'df_16': 0,
+ 'df_17': 0,
+ 'df_18': 0,
+ 'df_20': 0,
+ 'df_21': 0,
+ 'df_total': 0
+ }
def __init__(self):
+ self['spots_version'] = ADSB.VERSION
+ self['start_time'] = time.time()
+ self['start_time_string'] = time.ctime(self['start_time'])
pass
+ def __setitem__(self, key, value):
+ self.data[key] = value
+
+ def __getitem__(self, item):
+ return self.data[item]
+
def __str__(self):
st = "\n"
- st += "Preambles:{}\n".format(self.valid_preambles)
- st += "Valid CRC:{}\n".format(self.valid_crc)
- st += "Non valid CRC:{}\n".format(self.not_valid_crc)
+ st += "Preambles:{}\n".format(self['valid_preambles'])
+ st += "Valid CRC:{}\n".format(self['valid_crc'])
+ st += "Non valid CRC:{}\n".format(self['not_valid_crc'])
st += "Decoded messages: "
- st += "DF0: {} ".format(self.df_0)
- st += "DF4: {} ".format(self.df_4)
- st += "DF5: {} ".format(self.df_5)
- st += "DF16: {} ".format(self.df_16)
- st += "DF17: {} ".format(self.df_17)
- st += "DF18: {} ".format(self.df_18)
- st += "DF20: {} ".format(self.df_20)
- st += "DF21: {} ".format(self.df_21)
+ st += "DF0: {} ".format(self['df_0'])
+ st += "DF4: {} ".format(self['df_4'])
+ st += "DF5: {} ".format(self['df_5'])
+ st += "DF11: {} ".format(self['df_11'])
+ st += "DF16: {} ".format(self['df_16'])
+ st += "DF17: {} ".format(self['df_17'])
+ st += "DF18: {} ".format(self['df_18'])
+ st += "DF20: {} ".format(self['df_20'])
+ st += "DF21: {} ".format(self['df_21'])
+ st += "DF Total: {} ".format(self['df_total'])
return st
diff --git a/emitter.py b/emitter.py
new file mode 100644
index 0000000..2da7cc7
--- /dev/null
+++ b/emitter.py
@@ -0,0 +1,44 @@
+#!/usr/bin/env python
+
+from flask import Flask
+import socket
+
+
+__author__ = 'Wolfrax'
+
+app = Flask(__name__)
+cfg_server_address = 'localhost'
+cfg_server_port = 5051
+
+
+def get_msg(message):
+ sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+ sock.connect((cfg_server_address, cfg_server_port))
+ data = []
+ try:
+ sock.sendall(message)
+ while True:
+ stream = sock.recv(1024)
+ if not stream:
+ break
+ else:
+ data.append(stream)
+ data = ''.join(data)
+ finally:
+ sock.close()
+ return data
+
+
+@app.route("/spots/data")
+def spots_data():
+ return get_msg("GET DATA STR")
+
+
+@app.route("/spots/statistics")
+def spots_statistics():
+ return get_msg("GET STATISTICS STR")
+
+
+if __name__ == "__main__":
+ print "Will listen on {}:{}".format(cfg_server_address, cfg_server_port)
+ app.run(host='0.0.0.0', debug=True)
diff --git a/radar.conf b/radar.conf
new file mode 100644
index 0000000..c67e972
--- /dev/null
+++ b/radar.conf
@@ -0,0 +1,9 @@
+[program:radar]
+command=/home/pi/.virtualenvs/spots/bin/python /home/pi/app/spots/radar.py
+directory=/home/pi/app/spots
+autostart=true
+autorestart=true
+startretries=3
+stderr_logfile=/var/log/supervisor/radar.err
+stdout_logfile=/var/log/supervisor/radar.log
+user=root
\ No newline at end of file
diff --git a/radar.py b/radar.py
index ae8808f..94e63cc 100644
--- a/radar.py
+++ b/radar.py
@@ -8,6 +8,9 @@
import logging.handlers
import time
import sys
+import server
+
+__author__ = 'Wolfrax'
"""
This module implements the application using 2 classes
@@ -24,13 +27,13 @@
message queue (msgQ). The message passed from the tuner is in the format
[[signal strength (float), message (long)], [signal strength, message], ...]
-
+
signal strength is a simple measure of (max sample - min sample) in the preamble divided by max range (65535)
expressed in %. Thus signal_strength = 10 means that the diff of 'max sample' and 'min sample' is 10% of the
dynamic range.
-
+
message is the signal found by the tuner encoded as a long integer.
-
+
The radar object pick up the basic Squitter objects from the message queue (msgQ), decodes it further
(using Squitter.py) and store the resulting Squitter object into a 'blip dictionary' using the ICAO information as key.
@@ -48,7 +51,7 @@
class TextDisplay:
"""
This implements a table text display for Squitter messages using curses
-
+
Methods are:
add: add a new message + timestamp + count into the msgQ dictionary using ICAO address as key
The methods also traverse the msqQ objects to propagate message items to the latest on
@@ -56,6 +59,7 @@ class TextDisplay:
update_screen: format and add strings to the window form the msgQ
close: close the window environment in an orderly manner
"""
+
def __init__(self):
self.msgQ = {}
@@ -63,7 +67,7 @@ def __init__(self):
self.header_text = [[0, "ICAO"], [8, "Mode"], [14, "Sqwk"], [20, "Flight"], [29, "Alt"], [36, "Spd"],
[41, "Hdg"], [48, "Lat"], [57, "Long"], [64, "Sig%"], [70, "Msgs"], [77, "Ti "]]
self.last_pos = self.header_text[-1][0] + len(self.header_text[-1][1])
- self.header_underline = "-"*self.last_pos
+ self.header_underline = "-" * self.last_pos
self.header_spinner = "|/-\\"
self.update_cnt = 0
self.win = None
@@ -88,34 +92,24 @@ def update_screen(self):
self.win.addstr(0, ind[0], ind[1])
self.win.addch(0, self.last_pos, self.header_spinner[self.update_cnt % 4])
self.win.addstr(1, 0, self.header_underline)
+ self.win.clrtobot()
self.update_cnt += 1
row = 2
for key in self.msgQ.keys():
- for msg in self.msgQ[key]['msg']:
- self.win.addstr(row, self.header_text[0][0], msg.ICAO24, curses.color_pair(3))
- self.win.clrtoeol()
- if msg.downlink_format != 0:
- self.win.addstr(row, self.header_text[1][0], str(msg.downlink_format), curses.color_pair(3))
- if msg.Squawk != 0:
- self.win.addstr(row, self.header_text[2][0], "{:=04X}".format(msg.Squawk), curses.color_pair(3))
- if msg.call_sign != "":
- self.win.addstr(row, self.header_text[3][0], msg.call_sign, curses.color_pair(3))
- if msg.altitude != 0:
- self.win.addstr(row, self.header_text[4][0], str(int(round(msg.altitude))), curses.color_pair(3))
- if msg.velocity != 0:
- self.win.addstr(row, self.header_text[5][0], str(int(round(msg.velocity))), curses.color_pair(3))
- if msg.heading != 0:
- self.win.addstr(row, self.header_text[6][0], str(int(round(msg.heading))), curses.color_pair(3))
- if msg.latitude != 0:
- self.win.addstr(row, self.header_text[7][0], str(round(msg.latitude, 3)), curses.color_pair(3))
- if msg.longitude != 0:
- self.win.addstr(row, self.header_text[8][0], str(round(msg.longitude, 3)), curses.color_pair(3))
- if msg.signal_strength != 0:
- self.win.addstr(row, self.header_text[9][0], str(msg.signal_strength), curses.color_pair(3))
-
- self.win.addstr(row, self.header_text[10][0], str(self.msgQ[key]['msg_count']), curses.color_pair(2))
- self.win.addstr(row, self.header_text[11][0], str(self.msgQ[key]['timestamp']), curses.color_pair(1))
+ msg = self.msgQ[key]['msg']
+ self.win.addstr(row, self.header_text[0][0], msg['ICAO24'], curses.color_pair(3))
+ self.win.addstr(row, self.header_text[1][0], msg['downlink_format'], curses.color_pair(3))
+ self.win.addstr(row, self.header_text[2][0], msg['squawk'], curses.color_pair(3))
+ self.win.addstr(row, self.header_text[3][0], msg['call_sign'], curses.color_pair(3))
+ self.win.addstr(row, self.header_text[4][0], msg['altitude'], curses.color_pair(3))
+ self.win.addstr(row, self.header_text[5][0], msg['velocity'], curses.color_pair(3))
+ self.win.addstr(row, self.header_text[6][0], msg['heading'], curses.color_pair(3))
+ self.win.addstr(row, self.header_text[7][0], msg['latitude'], curses.color_pair(3))
+ self.win.addstr(row, self.header_text[8][0], msg['longitude'], curses.color_pair(3))
+ self.win.addstr(row, self.header_text[9][0], msg['signal_strength'], curses.color_pair(3))
+ self.win.addstr(row, self.header_text[10][0], self.msgQ[key]['msg_count'], curses.color_pair(2))
+ self.win.addstr(row, self.header_text[11][0], self.msgQ[key]['timestamp'], curses.color_pair(1))
row += 1
@@ -125,48 +119,19 @@ def clear_queue(self):
self.msgQ = {}
def add(self, ts, msg, cnt):
- # msgQ = {'abc': {'msg': [squitter1, squitter2, ...], 'timestamp': 0, 'msg_count': 1}, 'def':
- if msg.ICAO24 in self.msgQ:
- self.msgQ[msg.ICAO24]['msg'].append(msg)
- self.msgQ[msg.ICAO24]['timestamp'] = str(int(time.time() - ts))
- self.msgQ[msg.ICAO24]['msg_count'] = cnt
+ # msgQ = {'abc': {'msg': squitter, 'timestamp': '0', 'msg_count': '1'}, 'def':...
+ icao = msg['ICAO24']
+ if icao in self.msgQ:
+ self.msgQ[icao]['msg'].update(msg)
else:
- self.msgQ[msg.ICAO24] = {}
- self.msgQ[msg.ICAO24]['msg'] = [msg]
- self.msgQ[msg.ICAO24]['timestamp'] = str(int(time.time() - ts))
- self.msgQ[msg.ICAO24]['msg_count'] = cnt
+ self.msgQ[icao] = {}
+ self.msgQ[icao]['msg'] = msg
- # Scan list for call sign and Squawk information and copy this to new list items
- # If we don't do this the information will be lost when we remove the item
-
- for key in self.msgQ.keys():
- call_sign = ""
- velocity = altitude = heading = squawk = latitude = longitude = 0
- for item in self.msgQ[key]['msg']:
- if item.call_sign != "":
- call_sign = item.call_sign
- item.call_sign = call_sign
- if item.Squawk != 0:
- squawk = item.Squawk
- item.Squawk = squawk
- if item.velocity != 0:
- velocity = item.velocity
- item.velocity = velocity
- if item.altitude != 0:
- altitude = item.altitude
- item.altitude = altitude
- if item.heading != 0:
- heading = item.heading
- item.heading = heading
- if item.latitude != 0:
- latitude = item.latitude
- item.latitude = latitude
- if item.longitude != 0:
- longitude = item.longitude
- item.longitude = longitude
+ self.msgQ[icao]['timestamp'] = str(int(time.time() - ts))
+ self.msgQ[icao]['msg_count'] = str(cnt)
if len(self.msgQ) == self.max_row - 2:
- del self.msgQ[msg.ICAO24]
+ del self.msgQ[icao]
def close(self):
curses.curs_set(self.saved_cur)
@@ -179,13 +144,13 @@ class Radar(basic.ADSB, threading.Thread):
It implements a Queue (msgQ) where parsed squitter messages from the tuner are stored for processing.
Radar run through its own thread where the main loop (run) gets messages from msQ
-
+
The tuner_read method are a callback used by the tuner, this method is executed from the Tuner thread.
-
+
As messages are processed from the Queue they are stored into a dictionary (blips) using the ICAO address as index.
Messages in the dictionary have a timestamp and messages older than MAX_MSG_LIFETIME (seconds) are removed.
When messages are refreshed the timestamp are updated.
-
+
The radar use a Text User Interface (TUI) to display information to the end user.
"""
@@ -202,7 +167,33 @@ def __init__(self):
self.daemon = True # This is a daemon thread
self.logger = logging.getLogger('spots.Radar')
self.blip_timer = basic.RepeatTimer(1, self._scan_blips, "Radar blip timer")
+ self.stat_timer = basic.RepeatTimer(3600, self._show_stats, "Radar stat timer")
self.blip_timer.start()
+ self.stat_timer.start()
+
+ def _show_stats(self):
+ self.logger.info(str(basic.statistics))
+
+ @staticmethod
+ def get_statistics():
+ return basic.statistics.data
+
+ def get_blips_serialized(self):
+ # blips_series: {{'count': x, 'timestamp': y, 'altitude': 0, 'longitude': 13.755, ...},
+ # {'count': y, ...}}
+
+ result = []
+ self.lock.acquire()
+
+ for key in self.blips.keys():
+ elem = {'count': self.blips[key]['count'],
+ 'timestamp': str(int(time.time() - self.blips[key]['timestamp']))}
+ for msg_key in self.blips[key]['msg']:
+ elem.update({msg_key: self.blips[key]['msg'][msg_key]})
+ result.append(elem)
+
+ self.lock.release()
+ return result
def _scan_blips(self):
"""
@@ -213,11 +204,10 @@ def _scan_blips(self):
self._remove_old_blips()
for key in self.blips.keys():
- for item in self.blips[key]:
- if self.cfg_use_text_display:
- self.screen.add(item['timestamp'], item['msg'], item['count'])
- else:
- print item['msg']
+ if self.cfg_use_text_display:
+ self.screen.add(self.blips[key]['timestamp'], self.blips[key]['msg'], self.blips[key]['count'])
+ else:
+ print self.blips[key]['msg']
self.lock.release()
if self.cfg_use_text_display:
@@ -229,61 +219,45 @@ def _remove_old_blips(self):
Remove blips that has a timestamp older than specified level (normally 60 secs)
"""
for key in self.blips.keys():
- ind = 0
- for item in self.blips[key]:
- if (time.time() - item['timestamp']) >= self.cfg_max_blip_ttl:
- del self.blips[key][ind]
- ind += 1
- if not self.blips[key]:
+ if (time.time() - self.blips[key]['timestamp']) >= self.cfg_max_blip_ttl:
del self.blips[key]
def _blip_add(self, msg):
"""
Decdode and add msg to the blips dictionary with a timestamp using ICAO address as key.
- If an entry on the ICAO address exists, append the element. If it does not exists, create a new list.
-
+ If an entry on the ICAO address exists, update the element.
+
Note that a lock is needed before the blip dictionary is modified to avoid confusing the reader thread
"""
msg.decode()
+ icao = msg['ICAO24']
self.lock.acquire()
- # blip: {ICAO24: [{'timestamp': ts, 'count': n, 'msg': msg}, {'timestamp: ts, ...}]}
- if msg.ICAO24 in self.blips:
- item = {'timestamp': time.time(), 'count': self.blips[msg.ICAO24][-1]['count'] + 1, 'msg': msg}
- self.blips[msg.ICAO24].append(item)
+ # blip: {ICAO24: {'timestamp': ts, 'count': n, 'msg': msg},...}
+ if icao in self.blips:
+ self.blips[icao]['msg'].update(msg)
+ self.blips[icao]['timestamp'] = time.time()
+ self.blips[icao]['count'] += 1
else:
- item = {'timestamp': time.time(), 'count': 1, 'msg': msg}
- self.blips[msg.ICAO24] = [item]
+ self.blips[icao] = {'msg': msg, 'timestamp': time.time(), 'count': 1}
- # Decode latitude and longitude by finding 2 frames, 1 with odd CPR format + 1 with even CPR format
- for key in self.blips.keys():
- odd_msg = even_msg = None
- for item in self.blips[key]:
- if item['msg'].is_CPR():
- if item['msg'].is_odd_CPR():
- odd_msg = item['msg']
- else:
- even_msg = item['msg']
- if (even_msg is not None) and (odd_msg is not None):
- result = squitter.decodeCPR(odd_msg, even_msg)
- if result is not None:
- item['msg'].add_lat_long(result['latitude'], result['longitude'])
- else:
- result = squitter.decodeCPR_relative(odd_msg, even_msg)
- if result is not None:
- item['msg'].add_lat_long(result['latitude'], result['longitude'])
+ # Note, we need to decode for latitude/longitude after we have updated the message as the algorithm
+ # is dependent on 2 frames; odd and even
+ if not self.blips[icao]['msg'].decodeCPR():
+ pass # decodeCPR_relative is not validated yet and will likely yield wrong values
+ # self.blips[icao]['msg'].decodeCPR_relative()
self.lock.release()
if self.cfg_verbose_logging:
self.logger.info("{}".format(str(msg)))
def _blip_exist(self, msg):
- return msg.ICAO24 in self.blips
+ return msg['ICAO24'] in self.blips
def run(self):
"""
Main loop for the Radar object, here is were objects from the tuner is retrieved and checked before
adding to the blip dictionary.
-
+
Note that the crc is checked here versus different type of messages.
Messages are added to the blip dictionary if:
1. The crc is ok
@@ -299,22 +273,23 @@ def run(self):
while not self.finished.is_set():
msg = self.msgQ.get()
- if msg.downlink_format == self.DF_ALL_CALL_REPLY_11:
+ if msg.get_downlink_format() == self.DF_ALL_CALL_REPLY_11:
if msg.crc_ok:
self._blip_add(msg)
else:
if basic.ADSB.crc_2_int(msg.crc_sum) < 80 and self._blip_exist(msg):
msg.crc_ok = True
self._blip_add(msg)
- elif (msg.downlink_format == self.DF_ADSB_MSG_17 or msg.downlink_format == self.DF_EXTENDED_SQUITTER_18) \
- and msg.crc_ok:
+ elif msg.get_downlink_format() == self.DF_ADSB_MSG_17 and msg.crc_ok:
+ self._blip_add(msg)
+ elif msg.get_downlink_format() == self.DF_EXTENDED_SQUITTER_18 and msg.crc_ok:
self._blip_add(msg)
else: # All other DF have CRC xor'ed with ICAO address
if msg.crc_ok:
self._blip_add(msg)
else:
# we use the crc_sum for ICAO, this is tested in the decode method of Squitter
- msg.ICAO24 = msg.crc_sum
+ msg['ICAO24'] = msg.crc_sum
msg.crc_ok = True if self._blip_exist(msg) else False
if msg.crc_ok:
self._blip_add(msg)
@@ -326,6 +301,7 @@ def _die(self):
self.finished.set()
self.blip_timer.cancel()
+ self.stat_timer.cancel()
if self.cfg_use_text_display:
self.screen.close()
@@ -344,7 +320,7 @@ def tuner_read(self, msgs, stop=False):
self.msgQ.put(sq)
-def main():
+def run_Radar():
print "Spots version {}".format(basic.ADSB.VERSION)
sys.stderr = open("spots.err", 'w')
@@ -372,8 +348,18 @@ def main():
radar = Radar()
radar.start()
+ host = basic.ADSB.cfg_server_address
+ port = basic.ADSB.cfg_server_port
+
+ spots_server = server.SpotsServer((host, port), radar)
+ spots_server.start()
+
+ logger.info("Spots message server running, listening on {}:{}".format(host, port))
+
tuner1090.read(radar.tuner_read) # This is the main loop and main thread
+ spots_server.die()
+
if __name__ == '__main__':
- main()
+ run_Radar()
diff --git a/server.py b/server.py
new file mode 100644
index 0000000..aac887a
--- /dev/null
+++ b/server.py
@@ -0,0 +1,59 @@
+import json
+import SocketServer
+import socket
+import logging
+import threading
+
+__author__ = 'Wolfrax'
+
+"""
+This implements server functionality for spots, enabling clients to access spots data over network.
+
+The Spots server implements a threaded server, one thread per request. The requests follow a simple protocol
+ "GET DATA STR": message from the client will return the radar blip messages in serialized/json format
+ "GET STATISTICS STR": message from the client will return spots statistics in serialized/json format
+"""
+
+
+class TCPRequestHandler(SocketServer.BaseRequestHandler):
+ def handle(self):
+ cmd = self.request.recv(1024)
+
+ if cmd == "GET DATA STR":
+ response = self.server.radar.get_blips_serialized()
+ elif cmd == "GET STATISTICS STR":
+ response = self.server.radar.get_statistics()
+ else:
+ return
+
+ self.request.sendall(json.dumps(response))
+
+
+class SpotsServer(SocketServer.ThreadingMixIn, SocketServer.TCPServer):
+ """
+ A threaded TCP server handling requests through the TCPRequestHandler.
+ It will start a new thread for each request, these are managed by the method handle
+ """
+ def __init__(self, server_address, radar_object):
+ SocketServer.TCPServer.allow_reuse_address = True
+
+ SocketServer.TCPServer.__init__(self, server_address, TCPRequestHandler)
+
+ # reuse a local socket in TIME_WAIT state (SO_REUSEADDR)
+ self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
+
+ self.server_thread = threading.Thread(target=self.serve_forever)
+ self.server_thread.daemon = True
+ self.server_thread.name = "Socket server"
+
+ self.radar = radar_object
+ self.logger = logging.getLogger('spots.Server')
+ self.logger.info("Message server initialized")
+
+ def start(self):
+ self.server_thread.start()
+
+ def die(self):
+ self.shutdown()
+ self.server_close()
+
diff --git a/setup.py b/setup.py
index d173790..ee606a8 100644
--- a/setup.py
+++ b/setup.py
@@ -14,5 +14,5 @@
license='GPL',
author='Mats Melander',
author_email='mats.melander@gmail.com',
- description='A decoder for ADS-B messages on extended squitter at 1090MHz', requires=['numpy']
+ description='A decoder for ADS-B messages on extended squitter at 1090MHz', requires=['numpy', 'flask']
)
diff --git a/spots.html b/spots.html
new file mode 100644
index 0000000..cc54c36
--- /dev/null
+++ b/spots.html
@@ -0,0 +1,176 @@
+
+
+