314 lines
10 KiB
Python
314 lines
10 KiB
Python
"""
|
|
Stream beast raw data from a TCP server, convert to mode-s messages
|
|
"""
|
|
from __future__ import print_function, division
|
|
import os
|
|
import sys
|
|
import socket
|
|
import time
|
|
import pyModeS as pms
|
|
from threading import Thread
|
|
import traceback
|
|
import zmq
|
|
|
|
if sys.version_info > (3, 0):
|
|
PY_VERSION = 3
|
|
else:
|
|
PY_VERSION = 2
|
|
|
|
|
|
class BaseClient(Thread):
|
|
def __init__(self, host, port, datatype):
|
|
Thread.__init__(self)
|
|
self.host = host
|
|
self.port = port
|
|
self.buffer = []
|
|
self.socket = None
|
|
self.datatype = datatype
|
|
if self.datatype not in ["raw", "beast", "skysense"]:
|
|
print("datatype must be either raw, beast or skysense")
|
|
os._exit(1)
|
|
|
|
def connect(self):
|
|
self.socket = zmq.Context().socket(zmq.STREAM)
|
|
self.socket.setsockopt(zmq.LINGER, 0)
|
|
self.socket.setsockopt(zmq.RCVTIMEO, 2000)
|
|
self.socket.connect("tcp://%s:%s" % (self.host, self.port))
|
|
|
|
def read_raw_buffer(self):
|
|
""" Read raw ADS-B data type.
|
|
|
|
String strats with "*" and ends with ";". For example:
|
|
*5d484ba898f8c6;
|
|
*8d400cd5990d7e9a10043e5e6da0;
|
|
*a0001498be800030aa0000c7a75f;
|
|
"""
|
|
messages = []
|
|
|
|
msg_stop = False
|
|
self.current_msg = ""
|
|
for b in self.buffer:
|
|
if b == 59:
|
|
msg_stop = True
|
|
ts = time.time()
|
|
messages.append([self.current_msg, ts])
|
|
if b == 42:
|
|
msg_stop = False
|
|
self.current_msg = ""
|
|
|
|
if (not msg_stop) and (
|
|
48 <= b <= 57 or 65 <= b <= 70 or 97 <= b <= 102
|
|
):
|
|
self.current_msg = self.current_msg + chr(b)
|
|
|
|
self.buffer = []
|
|
|
|
return messages
|
|
|
|
def read_beast_buffer(self):
|
|
"""Handle mode-s beast data type.
|
|
|
|
<esc> "1" : 6 byte MLAT timestamp, 1 byte signal level,
|
|
2 byte Mode-AC
|
|
<esc> "2" : 6 byte MLAT timestamp, 1 byte signal level,
|
|
7 byte Mode-S short frame
|
|
<esc> "3" : 6 byte MLAT timestamp, 1 byte signal level,
|
|
14 byte Mode-S long frame
|
|
<esc> "4" : 6 byte MLAT timestamp, status data, DIP switch
|
|
configuration settings (not on Mode-S Beast classic)
|
|
<esc><esc>: true 0x1a
|
|
<esc> 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 <esc> 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:
|
|
ts = time.time()
|
|
|
|
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
|
|
|
|
df = pms.df(msg)
|
|
|
|
# skip incomplete message
|
|
if df in [0, 4, 5, 11] and len(msg) != 14:
|
|
continue
|
|
if df in [16, 17, 18, 19, 20, 21, 24] and len(msg) != 28:
|
|
continue
|
|
|
|
messages.append([msg, ts])
|
|
return messages
|
|
|
|
def read_skysense_buffer(self):
|
|
"""Skysense stream format.
|
|
|
|
::
|
|
|
|
----------------------------------------------------------------------------------
|
|
Field SS MS MS MS MS MS MS MS MS MS MS MS MS MS MS TS TS TS TS TS TS RS RS RS
|
|
----------------------------------------------------------------------------------
|
|
Position: 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
|
|
----------------------------------------------------------------------------------
|
|
|
|
SS field - Start character
|
|
Position 0:
|
|
1 byte = 8 bits
|
|
Start character '$'
|
|
|
|
MS field - Payload
|
|
Postion 1 through 14:
|
|
14 bytes = 112 bits
|
|
Mode-S payload
|
|
In case of DF types that only carry 7 bytes of information
|
|
position 8 through 14 are set to 0x00.
|
|
|
|
TS field - Time stamp
|
|
Position 15 through 20:
|
|
6 bytes = 48 bits
|
|
Time stamp with fields as:
|
|
|
|
Lock Status - Status of internal time keeping mechanism
|
|
Equal to 1 if operating normally
|
|
Bit 47 - 1 bit
|
|
|
|
Time of day in UTC seconds, between 0 and 86399
|
|
Bits 46 through 30 - 17 bits
|
|
|
|
Nanoseconds into current second, between 0 and 999999999
|
|
Bits 29 through 0 - 30 bits
|
|
|
|
RS field - Signal Level
|
|
Position 21 through 23:
|
|
3 bytes = 24 bits
|
|
RSSI (received signal strength indication) and relative
|
|
noise level with fields
|
|
|
|
RNL, Q12.4 unsigned fixed point binary with 4 fractional
|
|
bits and 8 integer bits.
|
|
This is and indication of the noise level of the message.
|
|
Roughly 40 counts per 10dBm.
|
|
Bits 23 through 12 - 12 bits
|
|
|
|
RSSI, Q12.4 unsigned fixed point binary with 4 fractional
|
|
bits and 8 integer bits.
|
|
This is an indication of the signal level of the received
|
|
message in ADC counts. Roughly 40 counts per 10dBm.
|
|
Bits 11 through 0 - 12 bits
|
|
"""
|
|
SS_MSGLENGTH = 24
|
|
SS_STARTCHAR = 0x24
|
|
|
|
if len(self.buffer) <= SS_MSGLENGTH:
|
|
return None
|
|
|
|
messages = []
|
|
while len(self.buffer) > SS_MSGLENGTH:
|
|
i = 0
|
|
if (
|
|
self.buffer[i] == SS_STARTCHAR
|
|
and self.buffer[i + SS_MSGLENGTH] == SS_STARTCHAR
|
|
):
|
|
i += 1
|
|
if self.buffer[i] >> 7:
|
|
# Long message
|
|
payload = self.buffer[i : i + 14]
|
|
else:
|
|
# Short message
|
|
payload = self.buffer[i : i + 7]
|
|
msg = "".join("%02X" % j for j in payload)
|
|
i += 14 # Both message types use 14 bytes
|
|
tsbin = self.buffer[i : i + 6]
|
|
sec = (
|
|
((tsbin[0] & 0x7F) << 10)
|
|
| (tsbin[1] << 2)
|
|
| (tsbin[2] >> 6)
|
|
)
|
|
nano = (
|
|
((tsbin[2] & 0x3F) << 24)
|
|
| (tsbin[3] << 16)
|
|
| (tsbin[4] << 8)
|
|
| tsbin[5]
|
|
)
|
|
ts = sec + nano * 1.0e-9
|
|
i += 6
|
|
# Signal and noise level - Don't care for now
|
|
i += 3
|
|
self.buffer = self.buffer[SS_MSGLENGTH:]
|
|
messages.append([msg, ts])
|
|
else:
|
|
self.buffer = self.buffer[1:]
|
|
return messages
|
|
|
|
def handle_messages(self, messages):
|
|
"""re-implement this method to handle the messages"""
|
|
for msg, t in messages:
|
|
print("%15.9f %s" % (t, msg))
|
|
|
|
def run(self):
|
|
self.connect()
|
|
|
|
while True:
|
|
try:
|
|
received = [i for i in self.socket.recv(4096)]
|
|
|
|
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 --
|
|
|
|
if self.datatype == "beast":
|
|
messages = self.read_beast_buffer()
|
|
elif self.datatype == "raw":
|
|
messages = self.read_raw_buffer()
|
|
elif self.datatype == "skysense":
|
|
messages = self.read_skysense_buffer()
|
|
|
|
if not messages:
|
|
continue
|
|
else:
|
|
self.handle_messages(messages)
|
|
|
|
time.sleep(0.001)
|
|
except Exception as e:
|
|
|
|
# Provides the user an option to supply the environment
|
|
# variable PYMODES_DEBUG to halt the execution
|
|
# for debugging purposes
|
|
debug_intent = os.environ.get("PYMODES_DEBUG", "false")
|
|
if debug_intent.lower() == "true":
|
|
traceback.print_exc()
|
|
sys.exit()
|
|
else:
|
|
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])
|
|
datatype = sys.argv[3]
|
|
client = BaseClient(host=host, port=port, datatype=datatype)
|
|
client.daemon = True
|
|
client.run()
|