Features:
- initial support for multiples tracks simulation Enhancements: - code profiling and optimization - faster lookup table based ppm + IQ generation - faster CRC24 computations - frame encoding Bug fixes: - callsign encoding - typos
@ -1,23 +1,39 @@
|
|||||||
|
""" This class is responsible for low level encoding of IQ sample
|
||||||
|
|
||||||
|
This program 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 of the License, or (at your option) any later
|
||||||
|
version.
|
||||||
|
This program 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, see <http://www.gnu.org/licenses/>.
|
||||||
|
"""
|
||||||
from CustomDecorators import *
|
from CustomDecorators import *
|
||||||
import numpy
|
import numpy
|
||||||
|
|
||||||
@Singleton
|
@Singleton
|
||||||
class ADSBLowLevelEncoder:
|
class ADSBLowLevelEncoder:
|
||||||
"""
|
|
||||||
Hamming and Manchester Encoding example
|
|
||||||
|
|
||||||
Author: Joel Addison
|
|
||||||
Date: March 2013
|
|
||||||
|
|
||||||
Functions to do (7,4) hamming encoding and decoding, including error detection
|
|
||||||
and correction.
|
|
||||||
Manchester encoding and decoding is also included, and by default will use
|
|
||||||
least bit ordering for the byte that is to be included in the array.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.adsb_frame_preamble = [0xA1,0x40]
|
self._adsb_frame_preamble = [0xA1,0x40]
|
||||||
self.adsb_frame_pause = [0]*70
|
self._adsb_frame_pause = [0]*4
|
||||||
|
|
||||||
|
# Build a manchester encoding lookup table
|
||||||
|
self._manchester_lookup = []
|
||||||
|
for i in range(256):
|
||||||
|
me = self.manchester_encode(i)
|
||||||
|
self._manchester_lookup.append([127*val for pair in zip(me, me) for val in pair])
|
||||||
|
|
||||||
|
# Build preamble and pause manchester encoded versions
|
||||||
|
me_bits = numpy.unpackbits(numpy.asarray(self._adsb_frame_preamble, dtype=numpy.uint8))
|
||||||
|
self._adsb_frame_preamble_IQ = [127*val for pair in zip(me_bits, me_bits) for val in pair]
|
||||||
|
self._adsb_frame_pause_IQ = self._adsb_frame_pause*16
|
||||||
|
|
||||||
|
self._len_pre_IQ = len(self._adsb_frame_preamble_IQ)
|
||||||
|
self._len_pause_IQ = len(self._adsb_frame_pause_IQ)
|
||||||
|
|
||||||
###############################################################
|
###############################################################
|
||||||
# Further work on fork
|
# Further work on fork
|
||||||
# Copyright (C) 2017 David Robinson
|
# Copyright (C) 2017 David Robinson
|
||||||
@ -38,65 +54,62 @@ class ADSBLowLevelEncoder:
|
|||||||
# Encode byte
|
# Encode byte
|
||||||
for i in range(7, -1, -1):
|
for i in range(7, -1, -1):
|
||||||
if self.extract_bit(byte, i):
|
if self.extract_bit(byte, i):
|
||||||
manchester_encoded.extend([0,1])
|
|
||||||
else:
|
|
||||||
manchester_encoded.extend([1,0])
|
manchester_encoded.extend([1,0])
|
||||||
|
else:
|
||||||
|
manchester_encoded.extend([0,1])
|
||||||
|
|
||||||
return manchester_encoded
|
return manchester_encoded
|
||||||
|
|
||||||
def frame_1090es_ppm_modulate(self, even, odd = []):
|
def frame_1090es_ppm_modulate_IQ(self, even, odd = []):
|
||||||
"""
|
"""
|
||||||
Args:
|
Args:
|
||||||
even and odd: The data frames that need to be converted to PPM
|
even and odd: The data frames that need to be converted to PPM
|
||||||
Returns:
|
Returns:
|
||||||
The bytearray of the PPM data
|
The bytearray of the IQ samples for PPM modulated data
|
||||||
"""
|
"""
|
||||||
ppm = [ ]
|
|
||||||
|
|
||||||
length_even = len(even)
|
length_even = len(even)
|
||||||
length_odd = len(odd)
|
length_odd = len(odd)
|
||||||
|
|
||||||
|
if (length_even != 0 and length_odd == 0):
|
||||||
|
IQ = bytearray(32*length_even+2*self._len_pause_IQ+self._len_pre_IQ)
|
||||||
|
pos = self._len_pause_IQ
|
||||||
|
IQ[pos:pos+self._len_pre_IQ] = self._adsb_frame_preamble_IQ
|
||||||
|
pos += self._len_pre_IQ
|
||||||
|
|
||||||
if (length_even != 0):
|
tmp = []
|
||||||
ppm.extend(self.adsb_frame_pause) # pause
|
for b in even:
|
||||||
ppm.extend(self.adsb_frame_preamble) # preamble
|
tmp.extend(self._manchester_lookup[b])
|
||||||
|
IQ[pos:pos+32*length_even] = tmp
|
||||||
|
|
||||||
for i in range(length_even):
|
return IQ
|
||||||
word16 = numpy.packbits(self.manchester_encode(~even[i]))
|
|
||||||
ppm.extend(word16[0:2])
|
|
||||||
|
|
||||||
ppm.extend(self.adsb_frame_pause) # pause
|
|
||||||
|
|
||||||
if (length_odd != 0):
|
elif (length_even != 0 and length_odd != 0):
|
||||||
ppm.extend(self.adsb_frame_pause) # pause
|
IQ = bytearray(32*(length_even+length_odd)+2*self._len_pre_IQ+3*self._len_pause_IQ)
|
||||||
ppm.extend(self.adsb_frame_preamble) # preamble
|
pos = self._len_pause_IQ
|
||||||
|
IQ[pos:pos+self._len_pre_IQ] = self._adsb_frame_preamble_IQ
|
||||||
|
pos += self._len_pre_IQ
|
||||||
|
|
||||||
for i in range(length_odd):
|
tmp = []
|
||||||
word16 = numpy.packbits(self.manchester_encode(~odd[i]))
|
for b in even:
|
||||||
ppm.extend(word16[0:2])
|
tmp.extend(self._manchester_lookup[b])
|
||||||
|
length_even_IQ = 32*length_even
|
||||||
|
IQ[pos:pos+length_even_IQ] = tmp
|
||||||
|
pos += length_even_IQ
|
||||||
|
|
||||||
ppm.extend(self.adsb_frame_pause) # pause
|
pos += self._len_pause_IQ
|
||||||
|
|
||||||
|
IQ[pos:pos+self._len_pre_IQ] = self._adsb_frame_preamble_IQ
|
||||||
|
pos += self._len_pre_IQ
|
||||||
|
|
||||||
|
tmp = []
|
||||||
|
for b in odd:
|
||||||
|
tmp.extend(self._manchester_lookup[b])
|
||||||
|
|
||||||
|
IQ[pos:pos+32*length_odd] = tmp
|
||||||
|
|
||||||
return bytearray(ppm)
|
return IQ
|
||||||
|
|
||||||
def hackrf_raw_IQ_format(self, ppm):
|
else:
|
||||||
"""
|
|
||||||
Args:
|
|
||||||
ppm: this is some data in ppm (pulse position modulation) which will be converted into
|
|
||||||
hackRF raw IQ sample format, ready to be broadcasted
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
bytearray: containing the IQ data
|
|
||||||
"""
|
|
||||||
signal = []
|
|
||||||
bits = numpy.unpackbits(numpy.asarray(ppm, dtype=numpy.uint8))
|
|
||||||
for bit in bits:
|
|
||||||
if bit == 1:
|
|
||||||
I = 127
|
|
||||||
Q = 127
|
|
||||||
else:
|
|
||||||
I = 0
|
|
||||||
Q = 0
|
|
||||||
signal.append(I)
|
|
||||||
signal.append(Q)
|
|
||||||
|
|
||||||
return bytearray(signal)
|
return None
|
||||||
|
@ -21,10 +21,10 @@ You should have received a copy of the GNU General Public License along with
|
|||||||
this program. If not, see <http://www.gnu.org/licenses/>.
|
this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import time
|
import time, datetime
|
||||||
import abc
|
import abc
|
||||||
import threading
|
import threading
|
||||||
|
from CustomDecorators import *
|
||||||
from ModeS import ModeS
|
from ModeS import ModeS
|
||||||
|
|
||||||
class AbstractTrajectorySimulatorBase(threading.Thread,abc.ABC):
|
class AbstractTrajectorySimulatorBase(threading.Thread,abc.ABC):
|
||||||
@ -38,60 +38,120 @@ class AbstractTrajectorySimulatorBase(threading.Thread,abc.ABC):
|
|||||||
|
|
||||||
self._do_stop = False
|
self._do_stop = False
|
||||||
|
|
||||||
|
self._position_message_period_us = 150000 # max should be 0.2s
|
||||||
|
self._velocity_message_period_us = 1200000 # max should be 1.3s
|
||||||
|
self._identitification_message_period_us = 10000000 # max should be 15s
|
||||||
|
self._squawk_message_period_us = 800000 # TODO : specs says that interval should be randomized between [0.7s;0.9s] and max is 1.0s
|
||||||
|
|
||||||
|
self._startruntime = None
|
||||||
|
self._simstep = 0
|
||||||
|
|
||||||
# Thread core function
|
# Thread core function
|
||||||
def run(self):
|
def run(self):
|
||||||
|
|
||||||
is_first_step = True
|
|
||||||
while not self._do_stop:
|
while not self._do_stop:
|
||||||
encoder_changed = False
|
now = datetime.datetime.now(datetime.timezone.utc)
|
||||||
# check if modeS encoder needs update
|
if (not self._startruntime or (now - self._startruntime).total_seconds() >= self._simstep*self.refresh_delay()):
|
||||||
if self._aircraftinfos.icao_changed or self._aircraftinfos.capability_changed or is_first_step:
|
self._simstep += 1
|
||||||
self._modeSencoder.icao = self._aircraftinfos.icao
|
encoder_changed = False
|
||||||
self._modeSencoder.ca = self._aircraftinfos.capability
|
|
||||||
encoder_changed = True
|
|
||||||
|
|
||||||
if encoder_changed or self._aircraftinfos.callsign_changed:
|
#self._broadcast_thread.getMutex().acquire()
|
||||||
self.df_callsign = self._modeSencoder.callsign_encode(self._aircraftinfos.callsign)
|
# check if modeS encoder needs update
|
||||||
self._broadcast_thread.replace_message("identification",self.df_callsign)
|
if self._aircraftinfos.icao_changed or self._aircraftinfos.capability_changed or not self._startruntime:
|
||||||
|
self._modeSencoder.icao = self._aircraftinfos.icao
|
||||||
|
self._modeSencoder.ca = self._aircraftinfos.capability
|
||||||
|
encoder_changed = True
|
||||||
|
|
||||||
if encoder_changed or self._aircraftinfos.squawk_changed:
|
if encoder_changed or self._aircraftinfos.callsign_changed:
|
||||||
self.frame_6116 = self._modeSencoder.modaA_encode(self._aircraftinfos.squawk)
|
self.df_callsign = self._modeSencoder.callsign_encode(self._aircraftinfos.callsign)
|
||||||
self._broadcast_thread.replace_message("register_6116",self.frame_6116)
|
self._broadcast_thread.replace_message("identification",self.df_callsign)
|
||||||
|
|
||||||
# message generation only if needed
|
if encoder_changed or self._aircraftinfos.squawk_changed:
|
||||||
if encoder_changed or self._aircraftinfos.on_surface_changed \
|
self.frame_6116 = self._modeSencoder.modaA_encode(self._aircraftinfos.squawk)
|
||||||
or self._aircraftinfos.lat_changed or self._aircraftinfos.lon_changed or self._aircraftinfos.alt_msl_changed \
|
self._broadcast_thread.replace_message("register_6116",self.frame_6116)
|
||||||
or self._aircraftinfos.type_code_changed or self._aircraftinfos.surveillance_status_changed or self._aircraftinfos.nicsupb_changed \
|
|
||||||
or self._aircraftinfos.timesync_changed:
|
|
||||||
if not self._aircraftinfos.on_surface:
|
|
||||||
(self.df_pos_even, self.df_pos_odd) = self._modeSencoder.df_encode_airborne_position(self._aircraftinfos.lat_deg, self._aircraftinfos.lon_deg, self._aircraftinfos.alt_msl_ft, \
|
|
||||||
self._aircraftinfos.type_code, self._aircraftinfos.surveillance_status, self._aircraftinfos.nicsupb, self._aircraftinfos.timesync)
|
|
||||||
self._broadcast_thread.replace_message("airborne_position",self.df_pos_even,self.df_pos_odd)
|
|
||||||
self._broadcast_thread.replace_message("surface_position",[], [])
|
|
||||||
else:
|
|
||||||
(self.df_pos_even, self.df_pos_odd) = self._modeSencoder.df_encode_surface_position(self._aircraftinfos.lat_deg, self._aircraftinfos.lon_deg, self._aircraftinfos.alt_msl_ft, \
|
|
||||||
self._aircraftinfos.type_code, self._aircraftinfos.surveillance_status, self._aircraftinfos.nicsupb, self._aircraftinfos.timesync)
|
|
||||||
self._broadcast_thread.replace_message("surface_position",self.df_pos_even,self.df_pos_odd)
|
|
||||||
self._broadcast_thread.replace_message("airborne_position",[], [])
|
|
||||||
|
|
||||||
if encoder_changed or self._aircraftinfos.speed_changed or self._aircraftinfos.track_angle_changed or self._aircraftinfos.vspeed_changed:
|
# message generation only if needed
|
||||||
self.df_velocity = self._modeSencoder.df_encode_ground_velocity(self._aircraftinfos.speed_kt, self._aircraftinfos.track_angle_deg, self._aircraftinfos.vspeed_ftpmin)
|
if encoder_changed or self._aircraftinfos.on_surface_changed \
|
||||||
self._broadcast_thread.replace_message("airborne_velocity",self.df_velocity)
|
or self._aircraftinfos.lat_changed or self._aircraftinfos.lon_changed or self._aircraftinfos.alt_msl_changed \
|
||||||
|
or self._aircraftinfos.type_code_changed or self._aircraftinfos.surveillance_status_changed or self._aircraftinfos.nicsupb_changed \
|
||||||
|
or self._aircraftinfos.timesync_changed:
|
||||||
|
if not self._aircraftinfos.on_surface:
|
||||||
|
(self.df_pos_even, self.df_pos_odd) = self._modeSencoder.df_encode_airborne_position(self._aircraftinfos.lat_deg, self._aircraftinfos.lon_deg, self._aircraftinfos.alt_msl_ft, \
|
||||||
|
self._aircraftinfos.type_code, self._aircraftinfos.surveillance_status, self._aircraftinfos.nicsupb, self._aircraftinfos.timesync)
|
||||||
|
self._broadcast_thread.replace_message("airborne_position",self.df_pos_even,self.df_pos_odd)
|
||||||
|
self._broadcast_thread.replace_message("surface_position",[], [])
|
||||||
|
else:
|
||||||
|
(self.df_pos_even, self.df_pos_odd) = self._modeSencoder.df_encode_surface_position(self._aircraftinfos.lat_deg, self._aircraftinfos.lon_deg, self._aircraftinfos.alt_msl_ft, \
|
||||||
|
self._aircraftinfos.type_code, self._aircraftinfos.surveillance_status, self._aircraftinfos.nicsupb, self._aircraftinfos.timesync)
|
||||||
|
self._broadcast_thread.replace_message("surface_position",self.df_pos_even,self.df_pos_odd)
|
||||||
|
self._broadcast_thread.replace_message("airborne_position",[], [])
|
||||||
|
|
||||||
is_first_step = False
|
if encoder_changed or self._aircraftinfos.speed_changed or self._aircraftinfos.track_angle_changed or self._aircraftinfos.vspeed_changed:
|
||||||
self.update_aircraftinfos() # update_aircraftinfos() : abstract method that need to be implemented i nderived classes
|
self.df_velocity = self._modeSencoder.df_encode_ground_velocity(self._aircraftinfos.speed_kt, self._aircraftinfos.track_angle_deg, self._aircraftinfos.vspeed_ftpmin)
|
||||||
time.sleep(self.refresh_delay()) # refresh_delay() : abstract method that need to be implemented i nderived classes
|
self._broadcast_thread.replace_message("airborne_velocity",self.df_velocity)
|
||||||
|
|
||||||
|
#self._broadcast_thread.getMutex().release()
|
||||||
|
if not self._startruntime:
|
||||||
|
self._startruntime = now
|
||||||
|
|
||||||
|
self.update_aircraftinfos() # update_aircraftinfos() : abstract method that need to be implemented in derived classes
|
||||||
|
|
||||||
|
#elapsed = (datetime.datetime.now(datetime.timezone.utc) - now).total_seconds()
|
||||||
|
#sleep_time = self.refresh_delay() - elapsed
|
||||||
|
#if sleep_time < 0.0:
|
||||||
|
# sleep_time = 0.0
|
||||||
|
time.sleep(0.05*self.refresh_delay()) # refresh_delay() : abstract method that need to be implemented in derived classes
|
||||||
|
|
||||||
# upon exit, reset _do_stop flag in case there is a new start
|
# upon exit, reset _do_stop flag in case there is a new start
|
||||||
self._do_stop = False
|
self._do_stop = False
|
||||||
|
|
||||||
def stop(self):
|
|
||||||
self._do_stop = True
|
|
||||||
|
|
||||||
@abc.abstractmethod
|
@abc.abstractmethod
|
||||||
def refresh_delay(self):
|
def refresh_delay(self):
|
||||||
...
|
...
|
||||||
|
|
||||||
@abc.abstractmethod
|
@abc.abstractmethod
|
||||||
def update_aircraftinfos(self):
|
def update_aircraftinfos(self):
|
||||||
...
|
...
|
||||||
|
|
||||||
|
def stop(self):
|
||||||
|
self._do_stop = True
|
||||||
|
|
||||||
|
@property
|
||||||
|
def elapsed_sim_time(self):
|
||||||
|
return self._simstep*self.refresh_delay()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def aircraftinfos(self):
|
||||||
|
return self._aircraftinfos
|
||||||
|
|
||||||
|
@property
|
||||||
|
def position_message_period_us(self):
|
||||||
|
return self._position_message_period_us
|
||||||
|
|
||||||
|
@position_message_period_us.setter
|
||||||
|
def position_message_period_us(self,value):
|
||||||
|
self._position_message_period_us = value
|
||||||
|
|
||||||
|
@property
|
||||||
|
def velocity_message_period_us(self):
|
||||||
|
return self._velocity_message_period_us
|
||||||
|
|
||||||
|
@velocity_message_period_us.setter
|
||||||
|
def velocity_message_period_us(self,value):
|
||||||
|
self._velocity_message_period_us = value
|
||||||
|
|
||||||
|
@property
|
||||||
|
def identitification_message_period_us(self):
|
||||||
|
return self._identitification_message_period_us
|
||||||
|
|
||||||
|
@identitification_message_period_us.setter
|
||||||
|
def identitification_message_period_us(self,value):
|
||||||
|
self._identitification_message_period_us = value
|
||||||
|
|
||||||
|
@property
|
||||||
|
def squawk_message_period_us(self):
|
||||||
|
return self._squawk_message_period_us
|
||||||
|
|
||||||
|
@squawk_message_period_us.setter
|
||||||
|
def squawk_message_period_us(self,value):
|
||||||
|
self._squawk_message_period_us = value
|
||||||
|
@ -15,8 +15,38 @@ this program. If not, see <http://www.gnu.org/licenses/>.
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import math
|
import math
|
||||||
|
import json
|
||||||
|
|
||||||
class AircraftInfos:
|
class AircraftInfos:
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_json(cls,filepath):
|
||||||
|
json_file = open(filepath,'r')
|
||||||
|
state_dic = json.load(json_file)
|
||||||
|
json_file.close()
|
||||||
|
|
||||||
|
icao_aa = state_dic["icao_aa"]
|
||||||
|
callsign = state_dic["callsign"]
|
||||||
|
squawk = state_dic["squawk"]
|
||||||
|
|
||||||
|
alt_ft = state_dic["alt_ft"]
|
||||||
|
lat_deg = state_dic["lat_deg"]
|
||||||
|
lon_deg = state_dic["lon_deg"]
|
||||||
|
speed_kph = state_dic["speed_kph"]
|
||||||
|
vspeed_ftpmin = state_dic["vspeed_ftpmin"]
|
||||||
|
maxloadfactor = state_dic["maxloadfactor"]
|
||||||
|
track_angle_deg = state_dic["track_angle_deg"]
|
||||||
|
capability = state_dic["capability"]
|
||||||
|
type_code = state_dic["type_code"]
|
||||||
|
surveillance_status = state_dic["surveillance_status"]
|
||||||
|
timesync = state_dic["timesync"]
|
||||||
|
nicsup = state_dic["nicsup"]
|
||||||
|
on_surface = state_dic["on_surface"]
|
||||||
|
|
||||||
|
return AircraftInfos(icao_aa,callsign,squawk,
|
||||||
|
lat_deg,lon_deg,alt_ft,speed_kph,vspeed_ftpmin,maxloadfactor,track_angle_deg, \
|
||||||
|
timesync,capability,type_code,surveillance_status,nicsup,on_surface)
|
||||||
|
|
||||||
def __init__(self,icao,callsign,squawk,
|
def __init__(self,icao,callsign,squawk,
|
||||||
lat_deg,lon_deg,alt_msl_ft,speed_kph,vspeed_ftpmin,maxloadfactor,track_angle_deg,
|
lat_deg,lon_deg,alt_msl_ft,speed_kph,vspeed_ftpmin,maxloadfactor,track_angle_deg,
|
||||||
timesync,capability,type_code,surveillance_status,nicsupb,on_surface):
|
timesync,capability,type_code,surveillance_status,nicsupb,on_surface):
|
||||||
|
@ -21,7 +21,7 @@ class FixedTrajectorySimulator(AbstractTrajectorySimulatorBase):
|
|||||||
super().__init__(mutex,broadcast_thread,aircrafinfos)
|
super().__init__(mutex,broadcast_thread,aircrafinfos)
|
||||||
|
|
||||||
def refresh_delay(self):
|
def refresh_delay(self):
|
||||||
return 0.005
|
return 0.05
|
||||||
|
|
||||||
def update_aircraftinfos(self):
|
def update_aircraftinfos(self):
|
||||||
pass
|
pass
|
||||||
|
@ -57,19 +57,14 @@ def hackrfTXCB(hackrf_transfer):
|
|||||||
|
|
||||||
@Singleton
|
@Singleton
|
||||||
class HackRfBroadcastThread(threading.Thread):
|
class HackRfBroadcastThread(threading.Thread):
|
||||||
def __init__(self,mutex,airborne_position_refresh_period = 150000):
|
def __init__(self,airborne_position_refresh_period = 150000):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self._mutex = mutex
|
self._mutex = threading.Lock()
|
||||||
|
|
||||||
self._lowlevelencoder = ADSBLowLevelEncoder()
|
self._lowlevelencoder = ADSBLowLevelEncoder()
|
||||||
|
|
||||||
self._messages = {}
|
self._messages_feed_threads = {}
|
||||||
# key : "name of message" value : ["data to be broadcasted", datetime of last broadcast, delay_between 2 messages of this type]
|
|
||||||
self._messages["identification"] = [None, None, 10000000] # max should be 15s
|
|
||||||
self._messages["register_6116"] = [None, None, 800000] # TODO : specs says that interval should be randomized between [0.7s;0.9s] and max is 1.0s
|
|
||||||
self._messages["airborne_position"] = [None, None, airborne_position_refresh_period] # max should be 0.2s
|
|
||||||
self._messages["surface_position"] = [None, None, 150000] # max should be 0.2s
|
|
||||||
self._messages["airborne_velocity"] = [None, None, 1200000] # max should be 1.3s
|
|
||||||
|
|
||||||
# Initialize pyHackRF library
|
# Initialize pyHackRF library
|
||||||
result = HackRF.initialize()
|
result = HackRF.initialize()
|
||||||
@ -86,6 +81,8 @@ class HackRfBroadcastThread(threading.Thread):
|
|||||||
if (result != LibHackRfReturnCode.HACKRF_SUCCESS):
|
if (result != LibHackRfReturnCode.HACKRF_SUCCESS):
|
||||||
print("Error :",result, ",", HackRF.getHackRfErrorCodeName(result))
|
print("Error :",result, ",", HackRF.getHackRfErrorCodeName(result))
|
||||||
|
|
||||||
|
self._hackrf_broadcaster.setCrystalPPM(0)
|
||||||
|
|
||||||
result = self._hackrf_broadcaster.setSampleRate(2000000)
|
result = self._hackrf_broadcaster.setSampleRate(2000000)
|
||||||
if (result != LibHackRfReturnCode.HACKRF_SUCCESS):
|
if (result != LibHackRfReturnCode.HACKRF_SUCCESS):
|
||||||
print("Error :",result, ",", HackRF.getHackRfErrorCodeName(result))
|
print("Error :",result, ",", HackRF.getHackRfErrorCodeName(result))
|
||||||
@ -95,7 +92,7 @@ class HackRfBroadcastThread(threading.Thread):
|
|||||||
print("Error :",result, ",", HackRF.getHackRfErrorCodeName(result))
|
print("Error :",result, ",", HackRF.getHackRfErrorCodeName(result))
|
||||||
|
|
||||||
#result = self.hackrf_broadcaster.setFrequency(868000000) # free frequency for over the air brodcast tests
|
#result = self.hackrf_broadcaster.setFrequency(868000000) # free frequency for over the air brodcast tests
|
||||||
result = self._hackrf_broadcaster.setFrequency(1090000000) # do not use 1090MHz for actual over the air broadcasting
|
result = self._hackrf_broadcaster.setFrequency(1090000000) # do not use 1090MHz for actual over the air broadcasting
|
||||||
# only if you use wire feed (you'll need attenuators in that case)
|
# only if you use wire feed (you'll need attenuators in that case)
|
||||||
if (result != LibHackRfReturnCode.HACKRF_SUCCESS):
|
if (result != LibHackRfReturnCode.HACKRF_SUCCESS):
|
||||||
print("Error :",result, ",", HackRF.getHackRfErrorCodeName(result))
|
print("Error :",result, ",", HackRF.getHackRfErrorCodeName(result))
|
||||||
@ -125,53 +122,113 @@ class HackRfBroadcastThread(threading.Thread):
|
|||||||
def stop(self):
|
def stop(self):
|
||||||
self._do_stop = True
|
self._do_stop = True
|
||||||
|
|
||||||
|
def getMutex(self):
|
||||||
|
return self._mutex
|
||||||
|
|
||||||
# updates the next data to be broadcaster for a given message type
|
# updates the next data to be broadcaster for a given message type
|
||||||
|
#@Timed
|
||||||
def replace_message(self,type,frame_even,frame_odd = []):
|
def replace_message(self,type,frame_even,frame_odd = []):
|
||||||
frame_ppm = self._lowlevelencoder.frame_1090es_ppm_modulate(frame_even, frame_odd)
|
|
||||||
frame_IQ = self._lowlevelencoder.hackrf_raw_IQ_format(frame_ppm)
|
|
||||||
|
|
||||||
# this will usuallyy be called from another thread, so mutex lock mecanism is used during update
|
frame_IQ = self._lowlevelencoder.frame_1090es_ppm_modulate_IQ(frame_even, frame_odd)
|
||||||
|
|
||||||
|
# this will usually be called from another thread, so mutex lock mecanism is used during update
|
||||||
|
|
||||||
self._mutex.acquire()
|
self._mutex.acquire()
|
||||||
self._messages[type][0] = frame_IQ
|
calling_thread = threading.current_thread()
|
||||||
|
if calling_thread in self._messages_feed_threads:
|
||||||
|
self._messages_feed_threads[calling_thread][type][0] = frame_IQ
|
||||||
self._mutex.release()
|
self._mutex.release()
|
||||||
|
|
||||||
def broadcast_one_message(self,data):
|
def register_track_simulation_thread(self,feeder_thread):
|
||||||
self._tx_context.last_tx_pos = 0
|
if feeder_thread in self._messages_feed_threads:
|
||||||
self._mutex.acquire()
|
print(feeder_thread,"already registred as a feeder")
|
||||||
self._tx_context.buffer_length = len(data)
|
else:
|
||||||
self._tx_context.buffer = (c_ubyte*self._tx_context.buffer_length).from_buffer_copy(data)
|
self._messages_feed_threads[feeder_thread] = {}
|
||||||
# TODO : need to evaluate if mutex protection is requiered during full broadcast or
|
|
||||||
# could be reduced to buffer filling (probably can be reduced)
|
|
||||||
# reduced version is when next line mutex.release() is uncommented and
|
|
||||||
# mutex release at the end of this method is commented
|
|
||||||
|
|
||||||
self._mutex.release()
|
# key : "name of message" value : ["data to be broadcasted", datetime of last broadcast, delay_between 2 messages of this type]
|
||||||
|
self._messages_feed_threads[feeder_thread]["identification"] = [None, None, feeder_thread.identitification_message_period_us]
|
||||||
|
self._messages_feed_threads[feeder_thread]["register_6116"] = [None, None, feeder_thread.squawk_message_period_us]
|
||||||
|
self._messages_feed_threads[feeder_thread]["airborne_position"] = [None, None, feeder_thread.position_message_period_us]
|
||||||
|
self._messages_feed_threads[feeder_thread]["surface_position"] = [None, None, feeder_thread.position_message_period_us]
|
||||||
|
self._messages_feed_threads[feeder_thread]["airborne_velocity"] = [None, None, feeder_thread.velocity_message_period_us]
|
||||||
|
|
||||||
result = self._hackrf_broadcaster.startTX(hackrfTXCB,self._tx_context)
|
def broadcast_data(self,data):
|
||||||
|
length = len(data)
|
||||||
|
if length != 0:
|
||||||
|
sleep_time = length*0.50e-6*(1.0+1e-6*self._hackrf_broadcaster.getCrystalPPM())
|
||||||
|
self._tx_context.last_tx_pos = 0
|
||||||
|
self._mutex.acquire()
|
||||||
|
self._tx_context.buffer_length = length
|
||||||
|
self._tx_context.buffer = (c_ubyte*self._tx_context.buffer_length).from_buffer_copy(data)
|
||||||
|
|
||||||
|
# TODO : need to evaluate if mutex protection is requiered during full broadcast or
|
||||||
|
# could be reduced to buffer filling (probably can be reduced)
|
||||||
|
# reduced version is when next line mutex.release() is uncommented and
|
||||||
|
# mutex release at the end of this method is commented
|
||||||
|
|
||||||
if (result != LibHackRfReturnCode.HACKRF_SUCCESS):
|
self._mutex.release()
|
||||||
print("Error :",result, ",", HackRF.getHackRfErrorCodeName(result))
|
|
||||||
|
|
||||||
while self._hackrf_broadcaster.isStreaming():
|
result = self._hackrf_broadcaster.startTX(hackrfTXCB,self._tx_context)
|
||||||
time.sleep(0.00001)
|
|
||||||
|
|
||||||
result = self._hackrf_broadcaster.stopTX()
|
if (result != LibHackRfReturnCode.HACKRF_SUCCESS):
|
||||||
if (result != LibHackRfReturnCode.HACKRF_SUCCESS):
|
print("Error :",result, ",", HackRF.getHackRfErrorCodeName(result))
|
||||||
print("Error :",result, ",", HackRF.getHackRfErrorCodeName(result))
|
|
||||||
|
while self._hackrf_broadcaster.isStreaming():
|
||||||
#self.mutex.release()
|
time.sleep(sleep_time)
|
||||||
|
|
||||||
|
result = self._hackrf_broadcaster.stopTX()
|
||||||
|
if (result != LibHackRfReturnCode.HACKRF_SUCCESS):
|
||||||
|
print("Error :",result, ",", HackRF.getHackRfErrorCodeName(result))
|
||||||
|
|
||||||
|
#self._mutex.release()
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
|
|
||||||
while not self._do_stop:
|
while not self._do_stop:
|
||||||
for k,v in self._messages.items():
|
#self._mutex.acquire()
|
||||||
now = datetime.datetime.now(datetime.timezone.utc)
|
now = datetime.datetime.now(datetime.timezone.utc)
|
||||||
# Time throttling : messages are broadcasted only at provided time intervall
|
plane_messages = bytearray()
|
||||||
# TODO : implement UTC syncing mecanism (requiered that the actual host clock is UTC synced)
|
sleep_time = 10.0
|
||||||
# which can be implemented to some accuracy level with ntp or GPS + PPS mecanisms
|
for thread_broadcast_schedule in self._messages_feed_threads.values():
|
||||||
if (v[0] != None and len(v[0]) > 0) and (v[1] == None or (now - v[1]) >= datetime.timedelta(seconds=v[2] // 1000000,microseconds=v[2] % 1000000)):
|
for v in thread_broadcast_schedule.values():
|
||||||
self.broadcast_one_message(v[0])
|
#now = datetime.datetime.now(datetime.timezone.utc)
|
||||||
v[1] = now
|
v2_sec = v[2]*1e-6
|
||||||
time.sleep(0.0001) # this loop will run at 10 kHz max
|
if v[1] != None:
|
||||||
|
remaining = v2_sec - (now - v[1]).total_seconds()
|
||||||
|
else:
|
||||||
|
remaining = -float('inf')
|
||||||
|
sleep_time = 0.0
|
||||||
|
# Time throttling : messages are broadcasted only at provided time intervall
|
||||||
|
# TODO : implement UTC syncing mecanism (requiered that the actual host clock is UTC synced) ?
|
||||||
|
# which may be implemented to some accuracy level with ntp or GPS + PPS mecanisms ? in Python ?
|
||||||
|
if (v[0] != None and len(v[0]) > 0) and remaining <= 0.0:
|
||||||
|
plane_messages.extend(v[0])
|
||||||
|
v[1] = now
|
||||||
|
elif remaining > 0.0:
|
||||||
|
remaining = math.fmod(remaining,v2_sec)
|
||||||
|
if remaining < sleep_time:
|
||||||
|
sleep_time = remaining
|
||||||
|
|
||||||
|
|
||||||
|
#print("sleep_time1",sleep_time)
|
||||||
|
bc_length = len(plane_messages)
|
||||||
|
if (bc_length > 0):
|
||||||
|
self.broadcast_data(plane_messages)
|
||||||
|
|
||||||
|
elasped = (datetime.datetime.now(datetime.timezone.utc) - now).total_seconds()
|
||||||
|
sleep_time -= elasped
|
||||||
|
|
||||||
|
if sleep_time < 0.0:
|
||||||
|
sleep_time = 0.0
|
||||||
|
elif sleep_time < 0.5:
|
||||||
|
sleep_time *= 0.1
|
||||||
|
else:
|
||||||
|
sleep_time = 0.5
|
||||||
|
|
||||||
|
time.sleep(0.1*sleep_time)
|
||||||
|
else:
|
||||||
|
time.sleep(0.000001)
|
||||||
|
#self._mutex.release()
|
||||||
|
|
||||||
# upon exit, reset _do_stop flag in case there is a new start
|
# upon exit, reset _do_stop flag in case there is a new start
|
||||||
self._do_stop = False
|
self._do_stop = False
|
213
ModeS.py
@ -1,6 +1,8 @@
|
|||||||
from ModeSLocation import ModeSLocation
|
from ModeSLocation import ModeSLocation
|
||||||
import math
|
import math
|
||||||
import numpy
|
import numpy
|
||||||
|
from CustomDecorators import *
|
||||||
|
|
||||||
###############################################################
|
###############################################################
|
||||||
# Further work on fork
|
# Further work on fork
|
||||||
# Copyright (C) 2017 David Robinson
|
# Copyright (C) 2017 David Robinson
|
||||||
@ -9,6 +11,9 @@ class ModeS:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self,df,icao,ca):
|
def __init__(self,df,icao,ca):
|
||||||
|
|
||||||
|
self._location = ModeSLocation()
|
||||||
|
|
||||||
self.df = df # as far as I understand specification, this should be :
|
self.df = df # as far as I understand specification, this should be :
|
||||||
# 17 if the broadcast source is an aircraft
|
# 17 if the broadcast source is an aircraft
|
||||||
# 18 if the broadcast source is some other ADSB facility (tower)
|
# 18 if the broadcast source is some other ADSB facility (tower)
|
||||||
@ -17,30 +22,42 @@ class ModeS:
|
|||||||
self.ca = ca # capability see §3.1.2.5.2.2.1
|
self.ca = ca # capability see §3.1.2.5.2.2.1
|
||||||
# (will usually be 5 for level 2 transponder and airborne)
|
# (will usually be 5 for level 2 transponder and airborne)
|
||||||
|
|
||||||
|
self._charmap = "#ABCDEFGHIJKLMNOPQRSTUVWXYZ#####_###############0123456789######"
|
||||||
|
self._crc_poly = 0x01FFF409
|
||||||
|
|
||||||
|
self._crc_lookup_T0 = [0]*256
|
||||||
|
|
||||||
|
crc = 0x1000000
|
||||||
|
i = 1
|
||||||
|
while(i < 256):
|
||||||
|
if crc & 0x1000000:
|
||||||
|
crc <<= 1
|
||||||
|
crc = crc ^ self._crc_poly
|
||||||
|
else:
|
||||||
|
crc <<= 1
|
||||||
|
|
||||||
|
for j in range(i):
|
||||||
|
self._crc_lookup_T0[i ^ j] = (crc ^ self._crc_lookup_T0[j])
|
||||||
|
|
||||||
|
i <<= 1
|
||||||
|
|
||||||
def df_frame_start(self):
|
def df_frame_start(self):
|
||||||
"""
|
"""
|
||||||
This will build the usual df frame start
|
This will build the usual df frame start and reserve a full frame with "0" for the reste of message
|
||||||
"""
|
"""
|
||||||
frame = []
|
frame = bytearray(14)
|
||||||
frame.append((self.df << 3) | self.ca)
|
frame[0] = (self.df << 3) | self.ca
|
||||||
frame.append((self.icao >> 16) & 0xff)
|
frame[1] = ((self.icao >> 16) & 0xff)
|
||||||
frame.append((self.icao >> 8) & 0xff)
|
frame[2] = ((self.icao >> 8) & 0xff)
|
||||||
frame.append((self.icao) & 0xff)
|
frame[3] = ((self.icao) & 0xff)
|
||||||
return frame
|
return frame
|
||||||
|
|
||||||
def df_frame_append_crc(self,frame):
|
|
||||||
frame_str = "{0:02x}{1:02x}{2:02x}{3:02x}{4:02x}{5:02x}{6:02x}{7:02x}{8:02x}{9:02x}{10:02x}".format(*frame[0:11])
|
|
||||||
frame_crc = self.bin2int(self.modes_crc(frame_str + "000000", encode=True))
|
|
||||||
frame.append((frame_crc >> 16) & 0xff)
|
|
||||||
frame.append((frame_crc >> 8) & 0xff)
|
|
||||||
frame.append((frame_crc) & 0xff)
|
|
||||||
|
|
||||||
# Ref :
|
# Ref :
|
||||||
# ICAO Annex 10 : Aeronautical Telecommunications
|
# ICAO Annex 10 : Aeronautical Telecommunications
|
||||||
# Volume IV : Surveillance and Collision Avoidance Systems
|
# Volume IV : Surveillance and Collision Avoidance Systems
|
||||||
# Figure C-1. Extended Squitter Airborne Position
|
# Figure C-1. Extended Squitter Airborne Position
|
||||||
# "Register 05_16"
|
# "Register 05_16"
|
||||||
|
#@Timed
|
||||||
def df_encode_airborne_position(self, lat, lon, alt, tc, ss, nicsb, timesync):
|
def df_encode_airborne_position(self, lat, lon, alt, tc, ss, nicsb, timesync):
|
||||||
"""
|
"""
|
||||||
This will encode even and odd frames from airborne position extended squitter message
|
This will encode even and odd frames from airborne position extended squitter message
|
||||||
@ -52,38 +69,38 @@ class ModeS:
|
|||||||
nicsb = NIC supplement-B (§C.2.3.2.5)
|
nicsb = NIC supplement-B (§C.2.3.2.5)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
location = ModeSLocation()
|
#encode height information
|
||||||
enc_alt = location.encode_alt_modes(alt, False)
|
enc_alt = self._location.encode_alt_modes(alt, False)
|
||||||
|
|
||||||
#encode that position
|
#encode that position
|
||||||
(evenenclat, evenenclon) = location.cpr_encode(lat, lon, False, False)
|
(evenenclat, evenenclon) = self._location.cpr_encode(lat, lon, False, False)
|
||||||
(oddenclat, oddenclon) = location.cpr_encode(lat, lon, True, False)
|
(oddenclat, oddenclon) = self._location.cpr_encode(lat, lon, True, False)
|
||||||
|
|
||||||
ff = 0
|
ff = 0
|
||||||
df_frame_even_bytes = self.df_frame_start()
|
df_frame_even_bytes = self.df_frame_start()
|
||||||
# data
|
# data
|
||||||
df_frame_even_bytes.append((tc<<3) | (ss<<1) | nicsb)
|
df_frame_even_bytes[4] = (tc<<3) | (ss<<1) | nicsb
|
||||||
df_frame_even_bytes.append((enc_alt>>4) & 0xff)
|
df_frame_even_bytes[5] = (enc_alt>>4) & 0xff
|
||||||
df_frame_even_bytes.append((enc_alt & 0xf) << 4 | (timesync<<3) | (ff<<2) | (evenenclat>>15))
|
df_frame_even_bytes[6] = (enc_alt & 0xf) << 4 | (timesync<<3) | (ff<<2) | (evenenclat>>15)
|
||||||
df_frame_even_bytes.append((evenenclat>>7) & 0xff)
|
df_frame_even_bytes[7] = (evenenclat>>7) & 0xff
|
||||||
df_frame_even_bytes.append(((evenenclat & 0x7f) << 1) | (evenenclon>>16))
|
df_frame_even_bytes[8] = ((evenenclat & 0x7f) << 1) | (evenenclon>>16)
|
||||||
df_frame_even_bytes.append((evenenclon>>8) & 0xff)
|
df_frame_even_bytes[9] = (evenenclon>>8) & 0xff
|
||||||
df_frame_even_bytes.append((evenenclon ) & 0xff)
|
df_frame_even_bytes[10] = (evenenclon ) & 0xff
|
||||||
|
|
||||||
self.df_frame_append_crc(df_frame_even_bytes)
|
self.df_frame_write_crc(df_frame_even_bytes)
|
||||||
|
|
||||||
ff = 1
|
ff = 1
|
||||||
df_frame_odd_bytes = self.df_frame_start()
|
df_frame_odd_bytes = self.df_frame_start()
|
||||||
# data
|
# data
|
||||||
df_frame_odd_bytes.append((tc<<3) | (ss<<1) | nicsb)
|
df_frame_odd_bytes[4] = (tc<<3) | (ss<<1) | nicsb
|
||||||
df_frame_odd_bytes.append((enc_alt>>4) & 0xff)
|
df_frame_odd_bytes[5] = (enc_alt>>4) & 0xff
|
||||||
df_frame_odd_bytes.append((enc_alt & 0xf) << 4 | (timesync<<3) | (ff<<2) | (oddenclat>>15))
|
df_frame_odd_bytes[6] = (enc_alt & 0xf) << 4 | (timesync<<3) | (ff<<2) | (oddenclat>>15)
|
||||||
df_frame_odd_bytes.append((oddenclat>>7) & 0xff)
|
df_frame_odd_bytes[7] = (oddenclat>>7) & 0xff
|
||||||
df_frame_odd_bytes.append(((oddenclat & 0x7f) << 1) | (oddenclon>>16))
|
df_frame_odd_bytes[8] = ((oddenclat & 0x7f) << 1) | (oddenclon>>16)
|
||||||
df_frame_odd_bytes.append((oddenclon>>8) & 0xff)
|
df_frame_odd_bytes[9] = (oddenclon>>8) & 0xff
|
||||||
df_frame_odd_bytes.append((oddenclon ) & 0xff)
|
df_frame_odd_bytes[10] = (oddenclon ) & 0xff
|
||||||
|
|
||||||
self.df_frame_append_crc(df_frame_odd_bytes)
|
self.df_frame_write_crc(df_frame_odd_bytes)
|
||||||
|
|
||||||
return (df_frame_even_bytes, df_frame_odd_bytes)
|
return (df_frame_even_bytes, df_frame_odd_bytes)
|
||||||
|
|
||||||
@ -101,16 +118,17 @@ class ModeS:
|
|||||||
# Volume IV : Surveillance and Collision Avoidance Systems
|
# Volume IV : Surveillance and Collision Avoidance Systems
|
||||||
# Figure C-3. Extended Squitter Status
|
# Figure C-3. Extended Squitter Status
|
||||||
# "Register 07_16"
|
# "Register 07_16"
|
||||||
|
#@Timed
|
||||||
def df_encode_extended_squitter_status(self, trs = 0x0, ats = 0x0):
|
def df_encode_extended_squitter_status(self, trs = 0x0, ats = 0x0):
|
||||||
df_frame = self.df_frame_start()
|
df_frame = self.df_frame_start()
|
||||||
|
|
||||||
df_frame.append((trs << 6) & 0x3 | (ats << 5) & 0x1)
|
df_frame[4] = ((trs << 6) & 0x3 | (ats << 5) & 0x1)
|
||||||
df_frame.extend([0]*6)
|
|
||||||
|
|
||||||
self.df_frame_append_crc(df_frame)
|
self.df_frame_write_crc(df_frame)
|
||||||
return df_frame
|
return df_frame
|
||||||
|
|
||||||
#From https://github.com/jaywilhelm/ADSB-Out_Python on 2019-08-18
|
#From https://github.com/jaywilhelm/ADSB-Out_Python on 2019-08-18
|
||||||
|
#@Timed
|
||||||
def df_encode_ground_velocity(self, ground_velocity_kt, track_angle_deg, vertical_rate):
|
def df_encode_ground_velocity(self, ground_velocity_kt, track_angle_deg, vertical_rate):
|
||||||
|
|
||||||
#1-5 downlink format
|
#1-5 downlink format
|
||||||
@ -161,15 +179,15 @@ class ModeS:
|
|||||||
|
|
||||||
dfvel = self.df_frame_start()
|
dfvel = self.df_frame_start()
|
||||||
# data
|
# data
|
||||||
dfvel.append((tc << 3) | st)
|
dfvel[4] = ((tc << 3) | st)
|
||||||
dfvel.append((ic << 7) | (resv_a << 6) | (NAC << 3) | (S_EW << 2) | ((V_EW >> 8) & 0x03))
|
dfvel[5] = ((ic << 7) | (resv_a << 6) | (NAC << 3) | (S_EW << 2) | ((V_EW >> 8) & 0x03))
|
||||||
dfvel.append(0xFF & V_EW)
|
dfvel[6] = (0xFF & V_EW)
|
||||||
dfvel.append((S_NS << 7) | ((V_NS >> 3))) #& 0x7F))
|
dfvel[7] = ((S_NS << 7) | ((V_NS >> 3))) #& 0x7F))
|
||||||
dfvel.append(((V_NS << 5) & 0xE0) | (VrSrc << 4) | (S_Vr << 3) | ((Vr >> 6) & 0x03))
|
dfvel[8] = (((V_NS << 5) & 0xE0) | (VrSrc << 4) | (S_Vr << 3) | ((Vr >> 6) & 0x03))
|
||||||
dfvel.append(((Vr << 2) & 0xFC) | (RESV_B))
|
dfvel[9] = (((Vr << 2) & 0xFC) | (RESV_B))
|
||||||
dfvel.append((S_Dif << 7) | (Dif))
|
dfvel[10] = ((S_Dif << 7) | (Dif))
|
||||||
|
|
||||||
self.df_frame_append_crc(dfvel)
|
self.df_frame_write_crc(dfvel)
|
||||||
|
|
||||||
return dfvel
|
return dfvel
|
||||||
|
|
||||||
@ -186,19 +204,17 @@ class ModeS:
|
|||||||
tc = 1 # §C.2.3.4
|
tc = 1 # §C.2.3.4
|
||||||
ec = 1 # §C.2.3.4
|
ec = 1 # §C.2.3.4
|
||||||
|
|
||||||
map = "#ABCDEFGHIJKLMNOPQRSTUVWXYZ#####_###############0123456789######"
|
|
||||||
|
|
||||||
dfname = self.df_frame_start()
|
dfname = self.df_frame_start()
|
||||||
# data
|
# data
|
||||||
dfname.append((tc << 3) | (ec))
|
dfname[4] = (tc << 3) | (ec)
|
||||||
dfname.append((0xFC & (int(map.find(csname[0])) << 2)) | (0x03 & (int(map.find(csname[1])) >> 6)))
|
dfname[5] = (0xFC & (int(self._charmap.find(csname[0])) << 2)) | (0x03 & (int(self._charmap.find(csname[1])) >> 4))
|
||||||
dfname.append((0xF0 & (int(map.find(csname[1])) << 4)) | (0x0F & (int(map.find(csname[2])) >> 2)))
|
dfname[6] = (0xF0 & (int(self._charmap.find(csname[1])) << 4)) | (0x0F & (int(self._charmap.find(csname[2])) >> 2))
|
||||||
dfname.append((0xF0 & (int(map.find(csname[2])) << 6)) | (0x3F & (int(map.find(csname[3])) >> 0)))
|
dfname[7] = (0xF0 & (int(self._charmap.find(csname[2])) << 6)) | (0x3F & (int(self._charmap.find(csname[3])) >> 0))
|
||||||
dfname.append((0xFC & (int(map.find(csname[4])) << 2)) | (0x03 & (int(map.find(csname[5])) >> 4)))
|
dfname[8] = (0xFC & (int(self._charmap.find(csname[4])) << 2)) | (0x03 & (int(self._charmap.find(csname[5])) >> 4))
|
||||||
dfname.append((0xF0 & (int(map.find(csname[5])) << 4)) | (0x0F & (int(map.find(csname[6])) >> 2)))
|
dfname[9] = (0xF0 & (int(self._charmap.find(csname[5])) << 4)) | (0x0F & (int(self._charmap.find(csname[6])) >> 2))
|
||||||
dfname.append((0xF0 & (int(map.find(csname[6])) << 6)) | (0x3F & (int(map.find(csname[7])) >> 0)))
|
dfname[10] = (0xF0 & (int(self._charmap.find(csname[6])) << 6)) | (0x3F & (int(self._charmap.find(csname[7])) >> 0))
|
||||||
|
|
||||||
self.df_frame_append_crc(dfname)
|
self.df_frame_write_crc(dfname)
|
||||||
|
|
||||||
return dfname
|
return dfname
|
||||||
|
|
||||||
@ -208,6 +224,7 @@ class ModeS:
|
|||||||
# Figure C-8a. Extended Squitter Aircraft Status
|
# Figure C-8a. Extended Squitter Aircraft Status
|
||||||
# "Register 61_16"
|
# "Register 61_16"
|
||||||
|
|
||||||
|
#@Timed
|
||||||
def modaA_encode(self,modeA_4096_code = "7000", emergency_state = 0x0):
|
def modaA_encode(self,modeA_4096_code = "7000", emergency_state = 0x0):
|
||||||
frame = self.df_frame_start()
|
frame = self.df_frame_start()
|
||||||
# data
|
# data
|
||||||
@ -216,7 +233,7 @@ class ModeS:
|
|||||||
# 1 : Emergency/Priority Status and Mode A Code
|
# 1 : Emergency/Priority Status and Mode A Code
|
||||||
# 2 : TCAS/ACAS RA Broadcast -> Figure C-8b : fields have different meaning
|
# 2 : TCAS/ACAS RA Broadcast -> Figure C-8b : fields have different meaning
|
||||||
# 3-7 : reserved
|
# 3-7 : reserved
|
||||||
frame.append((format_tc << 3) | st)
|
frame[4] = ((format_tc << 3) | st)
|
||||||
|
|
||||||
# Encode Squawk
|
# Encode Squawk
|
||||||
# ABCD (A:0-7, B:0-7, C:0-7, D:0-7)
|
# ABCD (A:0-7, B:0-7, C:0-7, D:0-7)
|
||||||
@ -277,67 +294,39 @@ class ModeS:
|
|||||||
elif squawk_str == "7500":
|
elif squawk_str == "7500":
|
||||||
emergency = 0x5
|
emergency = 0x5
|
||||||
|
|
||||||
frame.append(emergency << 5 | squawk_bits >> 8)
|
frame[5] = (emergency << 5 | squawk_bits >> 8)
|
||||||
frame.append(squawk_bits & 0xFF)
|
frame[6] = (squawk_bits & 0xFF)
|
||||||
|
|
||||||
frame.extend([0]*4)
|
self.df_frame_write_crc(frame)
|
||||||
|
|
||||||
self.df_frame_append_crc(frame)
|
|
||||||
|
|
||||||
return frame
|
return frame
|
||||||
|
|
||||||
###############################################################
|
#@Timed
|
||||||
# Copyright (C) 2015 Junzi Sun (TU Delft)
|
def df_frame_write_crc(self,frame):
|
||||||
# This program is free software: you can redistribute it and/or modify
|
crc = self.crc24_lu(frame[0:11])
|
||||||
# it under the terms of the GNU General Public License as published by
|
frame[11] = crc >> 16
|
||||||
# the Free Software Foundation, either version 3 of the License, or
|
frame[12] = (crc >> 8) & 0xFF
|
||||||
# (at your option) any later version.
|
frame[13] = crc & 0xFF
|
||||||
# This program 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, see <http://www.gnu.org/licenses/>.
|
|
||||||
###############################################################
|
|
||||||
|
|
||||||
# the polynominal generattor code for CRC
|
#@Timed
|
||||||
|
# CRC24 without lookup table
|
||||||
def modes_crc(self, msg, encode=False):
|
def crc24(self,msg):
|
||||||
"""Mode-S Cyclic Redundancy Check
|
crc = 0x0
|
||||||
Detect if bit error occurs in the Mode-S message
|
for b in msg:
|
||||||
Args:
|
crc ^= (b << 16)
|
||||||
msg (string): 28 bytes hexadecimal message string
|
for i in range(8):
|
||||||
encode (bool): True to encode the date only and return the checksum
|
crc <<= 1
|
||||||
Returns:
|
if crc & 0x1000000: crc ^= self._crc_poly
|
||||||
string: message checksum, or partity bits (encoder)
|
|
||||||
"""
|
|
||||||
|
|
||||||
GENERATOR = "1111111111111010000001001" # polynomial coefficients
|
return crc & 0xFFFFFF
|
||||||
|
|
||||||
msgbin = list(self.hex2bin(msg))
|
|
||||||
|
|
||||||
if encode:
|
#@Timed
|
||||||
msgbin[-24:] = ['0'] * 24
|
# CRC24 with lookup table (about 3x faster than non lookup version)
|
||||||
|
def crc24_lu(self,msg):
|
||||||
|
#print("len:",len(msg))
|
||||||
|
crc = 0x0
|
||||||
|
for b in msg:
|
||||||
|
tmp = (crc ^ (b << 16)) >> 16
|
||||||
|
crc = ((crc << 8) ^ self._crc_lookup_T0[tmp & 0xff])
|
||||||
|
|
||||||
# loop all bits, except last 24 piraty bits
|
return crc & 0xFFFFFF
|
||||||
for i in range(len(msgbin)-24):
|
|
||||||
# if 1, perform modulo 2 multiplication,
|
|
||||||
if msgbin[i] == '1':
|
|
||||||
for j in range(len(GENERATOR)):
|
|
||||||
# modulo 2 multiplication = XOR
|
|
||||||
msgbin[i+j] = str((int(msgbin[i+j]) ^ int(GENERATOR[j])))
|
|
||||||
|
|
||||||
# last 24 bits
|
|
||||||
reminder = ''.join(msgbin[-24:])
|
|
||||||
return reminder
|
|
||||||
|
|
||||||
def hex2bin(self, hexstr):
|
|
||||||
"""Convert a hexdecimal string to binary string, with zero fillings. """
|
|
||||||
scale = 16
|
|
||||||
num_of_bits = len(hexstr) * math.log(scale, 2)
|
|
||||||
binstr = bin(int(hexstr, scale))[2:].zfill(int(num_of_bits))
|
|
||||||
return binstr
|
|
||||||
|
|
||||||
def bin2int(self, binstr):
|
|
||||||
"""Convert a binary string to integer. """
|
|
||||||
return int(binstr, 2)
|
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import math
|
import math
|
||||||
|
from CustomDecorators import *
|
||||||
|
|
||||||
class ModeSLocation:
|
class ModeSLocation:
|
||||||
"""This class does ModeS/ADSB Location calulations"""
|
"""This class does ModeS/ADSB Location calulations"""
|
||||||
@ -89,6 +90,7 @@ class ModeSLocation:
|
|||||||
# encode CPR position
|
# encode CPR position
|
||||||
# https://adsb-decode-guide.readthedocs.io/en/latest/content/cpr.html
|
# https://adsb-decode-guide.readthedocs.io/en/latest/content/cpr.html
|
||||||
# compact position reporting
|
# compact position reporting
|
||||||
|
#@Timed
|
||||||
def cpr_encode(self, lat, lon, ctype, surface):
|
def cpr_encode(self, lat, lon, ctype, surface):
|
||||||
if surface is True:
|
if surface is True:
|
||||||
scalar = 2.**19
|
scalar = 2.**19
|
||||||
|
@ -20,32 +20,29 @@ from AbstractTrajectorySimulatorBase import AbstractTrajectorySimulatorBase
|
|||||||
class PseudoCircleTrajectorySimulator(AbstractTrajectorySimulatorBase):
|
class PseudoCircleTrajectorySimulator(AbstractTrajectorySimulatorBase):
|
||||||
def __init__(self,mutex,broadcast_thread,aircrafinfos):
|
def __init__(self,mutex,broadcast_thread,aircrafinfos):
|
||||||
super().__init__(mutex,broadcast_thread,aircrafinfos)
|
super().__init__(mutex,broadcast_thread,aircrafinfos)
|
||||||
self._starttime = datetime.datetime.now(datetime.timezone.utc)
|
|
||||||
|
|
||||||
self._lasttime = self._starttime
|
|
||||||
|
|
||||||
self._lat0 = aircrafinfos.lat_deg
|
self._lat0 = aircrafinfos.lat_deg
|
||||||
self._lon0 = aircrafinfos.lon_deg
|
self._lon0 = aircrafinfos.lon_deg
|
||||||
|
|
||||||
def refresh_delay(self):
|
def refresh_delay(self):
|
||||||
return 0.005
|
return 0.02
|
||||||
|
|
||||||
def update_aircraftinfos(self):
|
def update_aircraftinfos(self):
|
||||||
now = datetime.datetime.now(datetime.timezone.utc)
|
|
||||||
elapsed = (now - self._lasttime).total_seconds()
|
|
||||||
turn_rate = self.getTurnRate()
|
turn_rate = self.getTurnRate()
|
||||||
R = self.getTurnRadius()
|
R = self.getTurnRadius()
|
||||||
Rlat = (R/6371000.0)*(180.0/math.pi)
|
Rlat = (R/6371000.0)*(180.0/math.pi)
|
||||||
ta = self._aircraftinfos.track_angle_deg - (turn_rate*elapsed)*(180.0/math.pi)
|
ta = self._aircraftinfos.track_angle_deg - (turn_rate*self.refresh_delay())*(180.0/math.pi)
|
||||||
ta = math.fmod(ta,360.0)
|
ta = math.fmod(ta,360.0)
|
||||||
self._aircraftinfos.track_angle_deg = ta
|
self._aircraftinfos.track_angle_deg = ta # intermediate value and single assignment needed at time because of
|
||||||
|
# setter and change detection mecanism in AircraftInfo (somehox shitty)
|
||||||
|
# TODO : implement better mecanism
|
||||||
self._aircraftinfos.lat_deg = self._lat0 - Rlat*math.sin(self._aircraftinfos.track_angle_deg*math.pi/180.0)
|
self._aircraftinfos.lat_deg = self._lat0 - Rlat*math.sin(self._aircraftinfos.track_angle_deg*math.pi/180.0)
|
||||||
self._aircraftinfos.lon_deg = self._lon0 + Rlat/math.cos(self._aircraftinfos.lat_deg*math.pi/180.0)*math.cos(self._aircraftinfos.track_angle_deg*math.pi/180.0)
|
self._aircraftinfos.lon_deg = self._lon0 + Rlat/math.cos(self._aircraftinfos.lat_deg*math.pi/180.0)*math.cos(self._aircraftinfos.track_angle_deg*math.pi/180.0)
|
||||||
self._lasttime = now
|
#self._lasttime = now
|
||||||
|
|
||||||
def getTurnRate(self):
|
def getTurnRate(self):
|
||||||
tr = (9.81/self._aircraftinfos.speed_mps)*math.sqrt(self._aircraftinfos.maxloadfactor**2.0 - 1.0)
|
tr = (9.81/self._aircraftinfos.speed_mps)*math.sqrt(self._aircraftinfos.maxloadfactor**2.0 - 1.0)
|
||||||
return tr
|
return tr
|
||||||
|
|
||||||
def getTurnRadius(self):
|
def getTurnRadius(self):
|
||||||
return self._aircraftinfos.speed_mps/self.getTurnRate()
|
return self._aircraftinfos.speed_mps/self.getTurnRate()
|
||||||
|
56
README.md
@ -53,7 +53,7 @@ The hardware setup I'm using is pictured below. Please note the RF attenuators (
|
|||||||
The extra 1090MHz filter is probably not requiered as the flight aware dongle already features 1090 MHz filtering.
|
The extra 1090MHz filter is probably not requiered as the flight aware dongle already features 1090 MHz filtering.
|
||||||
My HackRF is fitted with a 0.5 ppm TCXO
|
My HackRF is fitted with a 0.5 ppm TCXO
|
||||||
|
|
||||||
![test setup image](./test-setup.jpg "test setup")
|
![test setup image](./images/test-setup.jpg "test setup")
|
||||||
|
|
||||||
The default hackrf settings in repo are :
|
The default hackrf settings in repo are :
|
||||||
- 1090 MHz
|
- 1090 MHz
|
||||||
@ -72,11 +72,47 @@ Those forged broadcasts may be used to spoof ATC, trigger TCAS or other maliciou
|
|||||||
|
|
||||||
## Command line examples
|
## Command line examples
|
||||||
|
|
||||||
|
####*Command line switches can be displayed with*
|
||||||
|
|
||||||
|
```
|
||||||
|
mathieu@devbox:~/Dev/matioupi/realtime-adsb-out$ ./realtime-adsb-out.py -h
|
||||||
|
Usage: ./realtime-adsb-out.py [options]
|
||||||
|
|
||||||
|
-h | --help Display help message.
|
||||||
|
--scenario <opt> Scenario mode with a provided scenario filepath
|
||||||
|
--icao <opt> Callsign in hex, Default:0x508035
|
||||||
|
--callsign <opt> Callsign (8 chars max), Default:DEADBEEF
|
||||||
|
--squawk <opt> 4-digits 4096 code squawk, Default:7000
|
||||||
|
--trajectorytype <opt> Type of simulated trajectory amongst :
|
||||||
|
fixed : steady aircraft
|
||||||
|
circle : pseudo circular flight
|
||||||
|
random : random positions inside circle area
|
||||||
|
waypoints : fly long flight path
|
||||||
|
Default:fixed
|
||||||
|
--lat <opt> Latitude for the plane in decimal degrees, Default:50.44994
|
||||||
|
--long <opt> Longitude for the place in decimal degrees. Default:30.5211
|
||||||
|
--altitude <opt> Altitude in decimal feet, Default:1500.0
|
||||||
|
--speed <opt> Airspeed in decimal kph, Default:300.0
|
||||||
|
--vspeed <opt> Vertical speed en ft/min, positive up, Default:0
|
||||||
|
--maxloadfactor Specify the max load factor for aircraft simulation. Default:1.45
|
||||||
|
--trackangle <opt> Track angle in decimal degrees. Default:0
|
||||||
|
--timesync <opt> 0/1, 0 indicates time not synchronous with UTC, Default:0
|
||||||
|
--capability <opt> Capability, Default:5
|
||||||
|
--typecode <opt> ADS-B message type, Default:11
|
||||||
|
--sstatus <opt> Surveillance status, Default:0
|
||||||
|
--nicsupplementb <opt> NIC supplement-B, Default:0
|
||||||
|
--surface Aircraft located on ground, Default:False
|
||||||
|
--waypoints <opt> Waypoints file for waypoints trajectory
|
||||||
|
--posrate <opt> position frame broadcast period in µs, Default: 150000
|
||||||
|
```
|
||||||
|
|
||||||
|
####*Single plane scenarii can be achieved with command line switches*
|
||||||
|
|
||||||
`./realtime-adsb-out.py --callsign 'FCKPUTIN' --alt 4500 --speed 600 --trajectorytype circle --maxloadfactor 1.03`
|
`./realtime-adsb-out.py --callsign 'FCKPUTIN' --alt 4500 --speed 600 --trajectorytype circle --maxloadfactor 1.03`
|
||||||
|
|
||||||
will generate a pseudo circular trajectory, flown at 4500 ft, 600 km/h and a load factor of 1.03.
|
will generate a pseudo circular trajectory, flown at 4500 ft, 600 km/h and a load factor of 1.03.
|
||||||
|
|
||||||
![circle mode example image](./adsb-out-circle.png "circle mode example")
|
![circle mode example image](./images/adsb-out-circle.png "circle mode example")
|
||||||
|
|
||||||
`./realtime-adsb-out.py --callsign 'FCKPUTIN' --alt 4500 --trajectorytype random`
|
`./realtime-adsb-out.py --callsign 'FCKPUTIN' --alt 4500 --trajectorytype random`
|
||||||
|
|
||||||
@ -84,13 +120,22 @@ will generate a random trajectory in a ~30s at specified (here default) speed ar
|
|||||||
track angle is randomized, speed is randomized, altitude is randomized. The default position frame broadcast period can be lowered in order to
|
track angle is randomized, speed is randomized, altitude is randomized. The default position frame broadcast period can be lowered in order to
|
||||||
produce a large numer of tracks in a given area
|
produce a large numer of tracks in a given area
|
||||||
|
|
||||||
![random mode example image](./adsb-out-random.png "random mode example")
|
![random mode example image](./images/adsb-out-random.png "random mode example")
|
||||||
|
|
||||||
|
####*More complex scenarii with multiple planes can be achieved though json configuration files*
|
||||||
|
|
||||||
|
`./realtime-adsb-out.py --scenario ./examples/scenario3.json`
|
||||||
|
|
||||||
|
![4 planes scenario example](./images/adsb-out-scenario3.png "4 planes scenario example")
|
||||||
|
|
||||||
|
The maximum number of planes that can be simulated has not been evaluated yet. It will depends on the refresh rate of each message type, etc.
|
||||||
|
Tests have been performed on a laptop computer, but with not too many tracks, it should be possible to run on lighter platforms such as Raspberry Pi.
|
||||||
|
|
||||||
## Reference documentation
|
## Reference documentation
|
||||||
|
|
||||||
All reference documentation from the repositories mentionned in the foreword.
|
All reference documentation from the repositories mentionned in the foreword.
|
||||||
|
|
||||||
https://mode-s.org/
|
[https://mode-s.org/](https://mode-s.org/)
|
||||||
|
|
||||||
*ICAO Annex 10, Aeronautical Telecommunications, Volume IV - Surveillance Radar and Collision Avoidance Systems* which at time of writing can be retrieved here:
|
*ICAO Annex 10, Aeronautical Telecommunications, Volume IV - Surveillance Radar and Collision Avoidance Systems* which at time of writing can be retrieved here:
|
||||||
- english version https://www.bazl.admin.ch/bazl/en/home/specialists/regulations-and-guidelines/legislation-and-directives/anhaenge-zur-konvention-der-internationalen-zivilluftfahrtorgani.html
|
- english version https://www.bazl.admin.ch/bazl/en/home/specialists/regulations-and-guidelines/legislation-and-directives/anhaenge-zur-konvention-der-internationalen-zivilluftfahrtorgani.html
|
||||||
@ -99,5 +144,8 @@ https://mode-s.org/
|
|||||||
*ICAO doc 9871 edition 1* which can be retrieved here (There is an edition 2 of this document but all seems to be behing paywalls):
|
*ICAO doc 9871 edition 1* which can be retrieved here (There is an edition 2 of this document but all seems to be behing paywalls):
|
||||||
- [ICAO doc 9871 edition 1](http://www.aviationchief.com/uploads/9/2/0/9/92098238/icao_doc_9871_-_technical_provisions_for_mode_s_-_advanced_edition_1.pdf)
|
- [ICAO doc 9871 edition 1](http://www.aviationchief.com/uploads/9/2/0/9/92098238/icao_doc_9871_-_technical_provisions_for_mode_s_-_advanced_edition_1.pdf)
|
||||||
|
|
||||||
|
Ghost in the Air(Traffic): On insecurity of ADS-B protocol and practical attacks on ADS-B devices (Andrei Costin, Aurélien Francillon):
|
||||||
|
[publication PDF hosted at eurocom.fr](https://www.s3.eurecom.fr/docs/bh12us_costin.pdf)
|
||||||
|
|
||||||
A DEFCON 20 video on Youtube that already highlighted some ADS-B weaknesses (and at this time, there was no HackRF):
|
A DEFCON 20 video on Youtube that already highlighted some ADS-B weaknesses (and at this time, there was no HackRF):
|
||||||
[DEFCON 20 (2012) - RenderMan - Hacker + Airplanes = No Good Can Come Of This](https://www.youtube.com/watch?v=mY2uiLfXmaI)
|
[DEFCON 20 (2012) - RenderMan - Hacker + Airplanes = No Good Can Come Of This](https://www.youtube.com/watch?v=mY2uiLfXmaI)
|
||||||
|
18
examples/A400M-094.json
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"icao_aa":"4B8214",
|
||||||
|
"callsign":"180094",
|
||||||
|
"squawk" : "7000",
|
||||||
|
"alt_ft": 35000.0,
|
||||||
|
"lat_deg": 50.24994,
|
||||||
|
"lon_deg": 30.0211,
|
||||||
|
"speed_kph": 780.0,
|
||||||
|
"vspeed_ftpmin": 0.0,
|
||||||
|
"maxloadfactor": 1.01,
|
||||||
|
"track_angle_deg": 0.0,
|
||||||
|
"capability": 5,
|
||||||
|
"type_code": 11,
|
||||||
|
"surveillance_status": 0,
|
||||||
|
"timesync": 0,
|
||||||
|
"nicsup": 0,
|
||||||
|
"on_surface": false
|
||||||
|
}
|
18
examples/A400M-110.json
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"icao_aa":"3B7761",
|
||||||
|
"callsign":"FRBAR",
|
||||||
|
"squawk" : "7000",
|
||||||
|
"alt_ft": 15500.0,
|
||||||
|
"lat_deg": 50.24994,
|
||||||
|
"lon_deg": 30.5211,
|
||||||
|
"speed_kph": 750.0,
|
||||||
|
"vspeed_ftpmin": 0.0,
|
||||||
|
"maxloadfactor": 1.06,
|
||||||
|
"track_angle_deg": 0.0,
|
||||||
|
"capability": 5,
|
||||||
|
"type_code": 11,
|
||||||
|
"surveillance_status": 0,
|
||||||
|
"timesync": 0,
|
||||||
|
"nicsup": 0,
|
||||||
|
"on_surface": false
|
||||||
|
}
|
18
examples/AN-IL78MP.json
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"icao_aa":"0x50FF5E",
|
||||||
|
"callsign":"R10002",
|
||||||
|
"squawk" : "7000",
|
||||||
|
"alt_ft": 6500.0,
|
||||||
|
"lat_deg": 50.44994,
|
||||||
|
"lon_deg": 30.8211,
|
||||||
|
"speed_kph": 500.0,
|
||||||
|
"vspeed_ftpmin": 0.0,
|
||||||
|
"maxloadfactor": 1.06,
|
||||||
|
"track_angle_deg": 0.0,
|
||||||
|
"capability": 5,
|
||||||
|
"type_code": 11,
|
||||||
|
"surveillance_status": 0,
|
||||||
|
"timesync": 0,
|
||||||
|
"nicsup": 0,
|
||||||
|
"on_surface": false
|
||||||
|
}
|
18
examples/MRYIA.json
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"icao_aa":"0x508035",
|
||||||
|
"callsign":"MRYIA",
|
||||||
|
"squawk": "7000",
|
||||||
|
"alt_ft": 4500.0,
|
||||||
|
"lat_deg": 50.44994,
|
||||||
|
"lon_deg": 30.5211,
|
||||||
|
"speed_kph": 600.0,
|
||||||
|
"vspeed_ftpmin": 0.0,
|
||||||
|
"maxloadfactor": 1.03,
|
||||||
|
"track_angle_deg": 0.0,
|
||||||
|
"capability": 5,
|
||||||
|
"type_code": 11,
|
||||||
|
"surveillance_status": 0,
|
||||||
|
"timesync": 0,
|
||||||
|
"nicsup": 0,
|
||||||
|
"on_surface": false
|
||||||
|
}
|
7
examples/scenario.json
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"plane1":
|
||||||
|
{
|
||||||
|
"filename":"./examples/MRYIA.json",
|
||||||
|
"trajectory_type":"circle"
|
||||||
|
}
|
||||||
|
}
|
12
examples/scenario2.json
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"plane1":
|
||||||
|
{
|
||||||
|
"filename":"./examples/MRYIA.json",
|
||||||
|
"trajectory_type":"circle"
|
||||||
|
},
|
||||||
|
"plane2":
|
||||||
|
{
|
||||||
|
"filename":"./examples/AN-IL78MP.json",
|
||||||
|
"trajectory_type":"circle"
|
||||||
|
}
|
||||||
|
}
|
22
examples/scenario3.json
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"plane1":
|
||||||
|
{
|
||||||
|
"filename":"./examples/MRYIA.json",
|
||||||
|
"trajectory_type":"circle"
|
||||||
|
},
|
||||||
|
"plane2":
|
||||||
|
{
|
||||||
|
"filename":"./examples/AN-IL78MP.json",
|
||||||
|
"trajectory_type":"circle"
|
||||||
|
},
|
||||||
|
"plane3":
|
||||||
|
{
|
||||||
|
"filename":"./examples/A400M-094.json",
|
||||||
|
"trajectory_type":"circle"
|
||||||
|
},
|
||||||
|
"plane4":
|
||||||
|
{
|
||||||
|
"filename":"./examples/A400M-110.json",
|
||||||
|
"trajectory_type":"circle"
|
||||||
|
}
|
||||||
|
}
|
Before Width: | Height: | Size: 1.2 MiB After Width: | Height: | Size: 1.2 MiB |
Before Width: | Height: | Size: 1.6 MiB After Width: | Height: | Size: 1.6 MiB |
BIN
images/adsb-out-scenario3.png
Normal file
After Width: | Height: | Size: 1.3 MiB |
Before Width: | Height: | Size: 93 KiB After Width: | Height: | Size: 93 KiB |
@ -14,8 +14,9 @@ You should have received a copy of the GNU General Public License along with
|
|||||||
this program. If not, see <http://www.gnu.org/licenses/>.
|
this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import sys, time, math
|
import sys, time, math, os
|
||||||
import threading
|
import threading, json
|
||||||
|
import traceback
|
||||||
|
|
||||||
from AircraftInfos import AircraftInfos
|
from AircraftInfos import AircraftInfos
|
||||||
from FixedTrajectorySimulator import FixedTrajectorySimulator
|
from FixedTrajectorySimulator import FixedTrajectorySimulator
|
||||||
@ -30,6 +31,7 @@ def usage(msg=False):
|
|||||||
if msg:print(msg)
|
if msg:print(msg)
|
||||||
print("Usage: %s [options]\n" % sys.argv[0])
|
print("Usage: %s [options]\n" % sys.argv[0])
|
||||||
print("-h | --help Display help message.")
|
print("-h | --help Display help message.")
|
||||||
|
print("--scenario <opt> Scenario mode with a provided scenario filepath")
|
||||||
print("--icao <opt> Callsign in hex, Default:0x508035")
|
print("--icao <opt> Callsign in hex, Default:0x508035")
|
||||||
print("--callsign <opt> Callsign (8 chars max), Default:DEADBEEF")
|
print("--callsign <opt> Callsign (8 chars max), Default:DEADBEEF")
|
||||||
print("--squawk <opt> 4-digits 4096 code squawk, Default:7000")
|
print("--squawk <opt> 4-digits 4096 code squawk, Default:7000")
|
||||||
@ -59,6 +61,21 @@ def usage(msg=False):
|
|||||||
|
|
||||||
sys.exit(2)
|
sys.exit(2)
|
||||||
|
|
||||||
|
def getTrackSimulationThread(trajectory_type,brodcast_thread,aircraftinfos,waypoints_file = None):
|
||||||
|
|
||||||
|
if trajectory_type == 'fixed':
|
||||||
|
return FixedTrajectorySimulator(brodcast_thread.getMutex(),brodcast_thread,aircraftinfos)
|
||||||
|
elif trajectory_type == 'circle':
|
||||||
|
return PseudoCircleTrajectorySimulator(brodcast_thread.getMutex(),brodcast_thread,aircraftinfos)
|
||||||
|
elif trajectory_type == 'random':
|
||||||
|
return RandomTrajectorySimulator(brodcast_thread.getMutex(),brodcast_thread,aircraftinfos)
|
||||||
|
elif trajectory_type == 'waypoints':
|
||||||
|
print("WaypointsTrajectorySimulator not implemented yet")
|
||||||
|
exit(-1)
|
||||||
|
return WaypointsTrajectorySimulator(brodcast_thread.getMutex(),brodcast_thread,aircraftinfos,waypoints_file)
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
|
|
||||||
# Default values
|
# Default values
|
||||||
@ -82,10 +99,10 @@ def main():
|
|||||||
trajectory_type = 'fixed'
|
trajectory_type = 'fixed'
|
||||||
waypoints_file = None
|
waypoints_file = None
|
||||||
posrate = 150000
|
posrate = 150000
|
||||||
|
scenariofile = None
|
||||||
try:
|
try:
|
||||||
(opts, args) = getopt(sys.argv[1:], 'h', \
|
(opts, args) = getopt(sys.argv[1:], 'h', \
|
||||||
['help','icao=','callsign=','squawk=','trajectorytype=','lat=','long=','altitude=','speed=','vspeed=','maxloadfactor=','trackangle=',
|
['help','scenario=','icao=','callsign=','squawk=','trajectorytype=','lat=','long=','altitude=','speed=','vspeed=','maxloadfactor=','trackangle=',
|
||||||
'timesync=','capability=','typecode=','sstatus=','nicsupplementb=','surface','posrate='
|
'timesync=','capability=','typecode=','sstatus=','nicsupplementb=','surface','posrate='
|
||||||
])
|
])
|
||||||
except GetoptError as err:
|
except GetoptError as err:
|
||||||
@ -94,6 +111,7 @@ def main():
|
|||||||
if len(opts) != 0:
|
if len(opts) != 0:
|
||||||
for (opt, arg) in opts:
|
for (opt, arg) in opts:
|
||||||
if opt in ('-h', '--help'):usage()
|
if opt in ('-h', '--help'):usage()
|
||||||
|
elif opt in ('--scenario'):scenariofile = arg
|
||||||
elif opt in ('--icao'):icao_aa = arg
|
elif opt in ('--icao'):icao_aa = arg
|
||||||
elif opt in ('--callsign'):callsign = arg
|
elif opt in ('--callsign'):callsign = arg
|
||||||
elif opt in ('--squawk'):squawk = arg
|
elif opt in ('--squawk'):squawk = arg
|
||||||
@ -117,38 +135,64 @@ def main():
|
|||||||
elif opt in ('--posrate'):posrate = int(arg)
|
elif opt in ('--posrate'):posrate = int(arg)
|
||||||
else:usage("Unknown option %s\n" % opt)
|
else:usage("Unknown option %s\n" % opt)
|
||||||
|
|
||||||
aircraftinfos = AircraftInfos(icao_aa,callsign,squawk, \
|
track_simulators = []
|
||||||
lat_deg,lon_deg,alt_ft,speed_kph,vspeed_ftpmin,maxloadfactor,track_angle_deg, \
|
broadcast_thread = HackRfBroadcastThread(posrate) # posrate would usally be used with random mode to generate load of tracks
|
||||||
timesync,capability,type_code,surveillance_status,nicsup,on_surface)
|
|
||||||
|
|
||||||
# TODO : the mutex would better be implemented as an object attribute in broadcast thread
|
if scenariofile == None:
|
||||||
mutex = threading.Lock()
|
print("Going to run in single plane from command line mode")
|
||||||
|
aircraftinfos = AircraftInfos(icao_aa,callsign,squawk, \
|
||||||
|
lat_deg,lon_deg,alt_ft,speed_kph,vspeed_ftpmin,maxloadfactor,track_angle_deg, \
|
||||||
|
timesync,capability,type_code,surveillance_status,nicsup,on_surface)
|
||||||
|
|
||||||
brodcast_thread = HackRfBroadcastThread(mutex,posrate) # posrate would usally be used with random mode to generate load of tracks
|
track_simulation = getTrackSimulationThread(trajectory_type,broadcast_thread,aircraftinfos,waypoints_file)
|
||||||
|
|
||||||
if trajectory_type == 'fixed':
|
track_simulators.append(track_simulation)
|
||||||
trajectory_simulator = FixedTrajectorySimulator(mutex,brodcast_thread,aircraftinfos)
|
|
||||||
elif trajectory_type == 'circle':
|
else:
|
||||||
trajectory_simulator = PseudoCircleTrajectorySimulator(mutex,brodcast_thread,aircraftinfos)
|
print("Going to run in json scenario mode from file "+os.path.abspath(scenariofile))
|
||||||
elif trajectory_type == 'random':
|
with open(scenariofile,'r') as json_file:
|
||||||
trajectory_simulator = RandomTrajectorySimulator(mutex,brodcast_thread,aircraftinfos)
|
scenario = json.load(json_file)
|
||||||
elif trajectory_type == 'waypoints':
|
|
||||||
print("WaypointsTrajectorySimulator not implemented yet")
|
for plane in scenario.values():
|
||||||
exit(-1)
|
plane_info = AircraftInfos.from_json(plane["filename"])
|
||||||
trajectory_simulator = WaypointsTrajectorySimulator(mutex,brodcast_thread,aircraftinfos,waypoints_file)
|
|
||||||
|
if "waypoints_file" in plane:
|
||||||
|
waypoints_file = plane["waypoints_file"]
|
||||||
|
|
||||||
|
track_simulation = getTrackSimulationThread(plane["trajectory_type"],broadcast_thread,plane_info,waypoints_file)
|
||||||
|
|
||||||
|
track_simulators.append(track_simulation)
|
||||||
|
|
||||||
|
print("scenario contains tracks simulation instructions for "+str(len(track_simulators))+" planes:")
|
||||||
|
for tsim in track_simulators:
|
||||||
|
print("callsign: "+tsim.aircraftinfos.callsign.ljust(9,' ')+"MSL altitude: "+"{:7.1f}".format(tsim.aircraftinfos.alt_msl_ft)+" ft")
|
||||||
|
|
||||||
|
for tsim in track_simulators:
|
||||||
|
broadcast_thread.register_track_simulation_thread(tsim)
|
||||||
|
|
||||||
while(val:=input("Type \'s + Enter\' to start the adsb-out simulation, and type \'s + Enter\' again to stop it:\n") != 's'):
|
while(val:=input("Type \'s + Enter\' to start the adsb-out simulation, and type \'s + Enter\' again to stop it:\n") != 's'):
|
||||||
time.sleep(0.05)
|
time.sleep(0.05)
|
||||||
|
|
||||||
trajectory_simulator.start()
|
# start all threads
|
||||||
brodcast_thread.start()
|
for tsim in track_simulators:
|
||||||
|
tsim.start()
|
||||||
|
|
||||||
|
broadcast_thread.start()
|
||||||
|
|
||||||
# user input loop. Todo : implement other commands ? (in that case don't forget to check if mutex protection is needed)
|
# user input loop. Todo : implement other commands ? (in that case don't forget to check if mutex protection is needed)
|
||||||
while(val:=input("") != 's'):
|
while(val:=input("") != 's'):
|
||||||
time.sleep(0.05)
|
time.sleep(0.05)
|
||||||
trajectory_simulator.stop()
|
|
||||||
brodcast_thread.stop()
|
# stop all threads
|
||||||
trajectory_simulator.join()
|
for tsim in track_simulators:
|
||||||
brodcast_thread.join()
|
tsim.stop()
|
||||||
|
|
||||||
|
broadcast_thread.stop()
|
||||||
|
|
||||||
|
# wait for all threads to terminate
|
||||||
|
for tsim in track_simulators:
|
||||||
|
tsim.join()
|
||||||
|
broadcast_thread.join()
|
||||||
|
|
||||||
print("reatime-adsb-out simulation is finished")
|
print("reatime-adsb-out simulation is finished")
|
||||||
|
|
||||||
|