diff --git a/ADSBLowLevelEncoder.py b/ADSBLowLevelEncoder.py index eebc745..8626ca8 100644 --- a/ADSBLowLevelEncoder.py +++ b/ADSBLowLevelEncoder.py @@ -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 . +""" from CustomDecorators import * import numpy @Singleton 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): - self.adsb_frame_preamble = [0xA1,0x40] - self.adsb_frame_pause = [0]*70 + self._adsb_frame_preamble = [0xA1,0x40] + 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 # Copyright (C) 2017 David Robinson @@ -38,65 +54,62 @@ class ADSBLowLevelEncoder: # Encode byte for i in range(7, -1, -1): if self.extract_bit(byte, i): - manchester_encoded.extend([0,1]) - else: manchester_encoded.extend([1,0]) + else: + manchester_encoded.extend([0,1]) return manchester_encoded - def frame_1090es_ppm_modulate(self, even, odd = []): + def frame_1090es_ppm_modulate_IQ(self, even, odd = []): """ Args: even and odd: The data frames that need to be converted to PPM Returns: - The bytearray of the PPM data + The bytearray of the IQ samples for PPM modulated data """ - ppm = [ ] length_even = len(even) 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): - ppm.extend(self.adsb_frame_pause) # pause - ppm.extend(self.adsb_frame_preamble) # preamble + tmp = [] + for b in even: + tmp.extend(self._manchester_lookup[b]) + IQ[pos:pos+32*length_even] = tmp - for i in range(length_even): - word16 = numpy.packbits(self.manchester_encode(~even[i])) - ppm.extend(word16[0:2]) - - ppm.extend(self.adsb_frame_pause) # pause + return IQ - if (length_odd != 0): - ppm.extend(self.adsb_frame_pause) # pause - ppm.extend(self.adsb_frame_preamble) # preamble + elif (length_even != 0 and length_odd != 0): + IQ = bytearray(32*(length_even+length_odd)+2*self._len_pre_IQ+3*self._len_pause_IQ) + 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): - word16 = numpy.packbits(self.manchester_encode(~odd[i])) - ppm.extend(word16[0:2]) + tmp = [] + for b in even: + 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): - """ - 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) + else: - return bytearray(signal) \ No newline at end of file + return None diff --git a/AbstractTrajectorySimulatorBase.py b/AbstractTrajectorySimulatorBase.py index 3ba8863..22b38e4 100644 --- a/AbstractTrajectorySimulatorBase.py +++ b/AbstractTrajectorySimulatorBase.py @@ -21,10 +21,10 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . """ -import time +import time, datetime import abc import threading - +from CustomDecorators import * from ModeS import ModeS class AbstractTrajectorySimulatorBase(threading.Thread,abc.ABC): @@ -38,60 +38,120 @@ class AbstractTrajectorySimulatorBase(threading.Thread,abc.ABC): 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 def run(self): - is_first_step = True while not self._do_stop: - encoder_changed = False - # check if modeS encoder needs update - if self._aircraftinfos.icao_changed or self._aircraftinfos.capability_changed or is_first_step: - self._modeSencoder.icao = self._aircraftinfos.icao - self._modeSencoder.ca = self._aircraftinfos.capability - encoder_changed = True + now = datetime.datetime.now(datetime.timezone.utc) + if (not self._startruntime or (now - self._startruntime).total_seconds() >= self._simstep*self.refresh_delay()): + self._simstep += 1 + encoder_changed = False - if encoder_changed or self._aircraftinfos.callsign_changed: - self.df_callsign = self._modeSencoder.callsign_encode(self._aircraftinfos.callsign) - self._broadcast_thread.replace_message("identification",self.df_callsign) + #self._broadcast_thread.getMutex().acquire() + # check if modeS encoder needs update + 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: - self.frame_6116 = self._modeSencoder.modaA_encode(self._aircraftinfos.squawk) - self._broadcast_thread.replace_message("register_6116",self.frame_6116) + if encoder_changed or self._aircraftinfos.callsign_changed: + self.df_callsign = self._modeSencoder.callsign_encode(self._aircraftinfos.callsign) + self._broadcast_thread.replace_message("identification",self.df_callsign) - # message generation only if needed - if encoder_changed or self._aircraftinfos.on_surface_changed \ - 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",[], []) + if encoder_changed or self._aircraftinfos.squawk_changed: + self.frame_6116 = self._modeSencoder.modaA_encode(self._aircraftinfos.squawk) + self._broadcast_thread.replace_message("register_6116",self.frame_6116) - if encoder_changed or self._aircraftinfos.speed_changed or self._aircraftinfos.track_angle_changed or self._aircraftinfos.vspeed_changed: - self.df_velocity = self._modeSencoder.df_encode_ground_velocity(self._aircraftinfos.speed_kt, self._aircraftinfos.track_angle_deg, self._aircraftinfos.vspeed_ftpmin) - self._broadcast_thread.replace_message("airborne_velocity",self.df_velocity) + # message generation only if needed + if encoder_changed or self._aircraftinfos.on_surface_changed \ + 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 - self.update_aircraftinfos() # update_aircraftinfos() : abstract method that need to be implemented i nderived classes - time.sleep(self.refresh_delay()) # refresh_delay() : abstract method that need to be implemented i nderived classes + if encoder_changed or self._aircraftinfos.speed_changed or self._aircraftinfos.track_angle_changed or self._aircraftinfos.vspeed_changed: + self.df_velocity = self._modeSencoder.df_encode_ground_velocity(self._aircraftinfos.speed_kt, self._aircraftinfos.track_angle_deg, self._aircraftinfos.vspeed_ftpmin) + 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 self._do_stop = False - def stop(self): - self._do_stop = True - @abc.abstractmethod def refresh_delay(self): ... @abc.abstractmethod def update_aircraftinfos(self): - ... \ No newline at end of file + ... + + 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 diff --git a/AircraftInfos.py b/AircraftInfos.py index 5e4170a..2f7fed1 100644 --- a/AircraftInfos.py +++ b/AircraftInfos.py @@ -15,8 +15,38 @@ this program. If not, see . """ import math +import json 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, lat_deg,lon_deg,alt_msl_ft,speed_kph,vspeed_ftpmin,maxloadfactor,track_angle_deg, timesync,capability,type_code,surveillance_status,nicsupb,on_surface): diff --git a/FixedTrajectorySimulator.py b/FixedTrajectorySimulator.py index b282996..1c0f5f0 100644 --- a/FixedTrajectorySimulator.py +++ b/FixedTrajectorySimulator.py @@ -21,7 +21,7 @@ class FixedTrajectorySimulator(AbstractTrajectorySimulatorBase): super().__init__(mutex,broadcast_thread,aircrafinfos) def refresh_delay(self): - return 0.005 + return 0.05 def update_aircraftinfos(self): - pass \ No newline at end of file + pass diff --git a/HackRfBroadcastThread.py b/HackRfBroadcastThread.py index e022d7f..4b91414 100644 --- a/HackRfBroadcastThread.py +++ b/HackRfBroadcastThread.py @@ -57,19 +57,14 @@ def hackrfTXCB(hackrf_transfer): @Singleton class HackRfBroadcastThread(threading.Thread): - def __init__(self,mutex,airborne_position_refresh_period = 150000): + def __init__(self,airborne_position_refresh_period = 150000): super().__init__() - self._mutex = mutex + self._mutex = threading.Lock() self._lowlevelencoder = ADSBLowLevelEncoder() - self._messages = {} - # 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 + self._messages_feed_threads = {} + # Initialize pyHackRF library result = HackRF.initialize() @@ -86,6 +81,8 @@ class HackRfBroadcastThread(threading.Thread): if (result != LibHackRfReturnCode.HACKRF_SUCCESS): print("Error :",result, ",", HackRF.getHackRfErrorCodeName(result)) + self._hackrf_broadcaster.setCrystalPPM(0) + result = self._hackrf_broadcaster.setSampleRate(2000000) if (result != LibHackRfReturnCode.HACKRF_SUCCESS): print("Error :",result, ",", HackRF.getHackRfErrorCodeName(result)) @@ -95,7 +92,7 @@ class HackRfBroadcastThread(threading.Thread): 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(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) if (result != LibHackRfReturnCode.HACKRF_SUCCESS): print("Error :",result, ",", HackRF.getHackRfErrorCodeName(result)) @@ -125,53 +122,113 @@ class HackRfBroadcastThread(threading.Thread): def stop(self): self._do_stop = True + def getMutex(self): + return self._mutex + # updates the next data to be broadcaster for a given message type + #@Timed 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._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() - def broadcast_one_message(self,data): - self._tx_context.last_tx_pos = 0 - self._mutex.acquire() - self._tx_context.buffer_length = len(data) - 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 + def register_track_simulation_thread(self,feeder_thread): + if feeder_thread in self._messages_feed_threads: + print(feeder_thread,"already registred as a feeder") + else: + self._messages_feed_threads[feeder_thread] = {} - 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): - print("Error :",result, ",", HackRF.getHackRfErrorCodeName(result)) + self._mutex.release() - while self._hackrf_broadcaster.isStreaming(): - time.sleep(0.00001) + result = self._hackrf_broadcaster.startTX(hackrfTXCB,self._tx_context) - result = self._hackrf_broadcaster.stopTX() - if (result != LibHackRfReturnCode.HACKRF_SUCCESS): - print("Error :",result, ",", HackRF.getHackRfErrorCodeName(result)) - - #self.mutex.release() + if (result != LibHackRfReturnCode.HACKRF_SUCCESS): + print("Error :",result, ",", HackRF.getHackRfErrorCodeName(result)) + + while self._hackrf_broadcaster.isStreaming(): + 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): + while not self._do_stop: - for k,v in self._messages.items(): - now = datetime.datetime.now(datetime.timezone.utc) - # 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 can be implemented to some accuracy level with ntp or GPS + PPS mecanisms - 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)): - self.broadcast_one_message(v[0]) - v[1] = now - time.sleep(0.0001) # this loop will run at 10 kHz max + #self._mutex.acquire() + now = datetime.datetime.now(datetime.timezone.utc) + plane_messages = bytearray() + sleep_time = 10.0 + for thread_broadcast_schedule in self._messages_feed_threads.values(): + for v in thread_broadcast_schedule.values(): + #now = datetime.datetime.now(datetime.timezone.utc) + v2_sec = v[2]*1e-6 + 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 self._do_stop = False \ No newline at end of file diff --git a/ModeS.py b/ModeS.py index 2d1f66b..2095abf 100644 --- a/ModeS.py +++ b/ModeS.py @@ -1,6 +1,8 @@ from ModeSLocation import ModeSLocation import math import numpy +from CustomDecorators import * + ############################################################### # Further work on fork # Copyright (C) 2017 David Robinson @@ -9,6 +11,9 @@ class ModeS: """ def __init__(self,df,icao,ca): + + self._location = ModeSLocation() + self.df = df # as far as I understand specification, this should be : # 17 if the broadcast source is an aircraft # 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 # (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): """ - 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.append((self.df << 3) | self.ca) - frame.append((self.icao >> 16) & 0xff) - frame.append((self.icao >> 8) & 0xff) - frame.append((self.icao) & 0xff) + frame = bytearray(14) + frame[0] = (self.df << 3) | self.ca + frame[1] = ((self.icao >> 16) & 0xff) + frame[2] = ((self.icao >> 8) & 0xff) + frame[3] = ((self.icao) & 0xff) 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 : # ICAO Annex 10 : Aeronautical Telecommunications # Volume IV : Surveillance and Collision Avoidance Systems # Figure C-1. Extended Squitter Airborne Position # "Register 05_16" - + #@Timed 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 @@ -52,38 +69,38 @@ class ModeS: nicsb = NIC supplement-B (§C.2.3.2.5) """ - location = ModeSLocation() - enc_alt = location.encode_alt_modes(alt, False) + #encode height information + enc_alt = self._location.encode_alt_modes(alt, False) #encode that position - (evenenclat, evenenclon) = location.cpr_encode(lat, lon, False, False) - (oddenclat, oddenclon) = location.cpr_encode(lat, lon, True, False) + (evenenclat, evenenclon) = self._location.cpr_encode(lat, lon, False, False) + (oddenclat, oddenclon) = self._location.cpr_encode(lat, lon, True, False) ff = 0 df_frame_even_bytes = self.df_frame_start() # data - df_frame_even_bytes.append((tc<<3) | (ss<<1) | nicsb) - df_frame_even_bytes.append((enc_alt>>4) & 0xff) - df_frame_even_bytes.append((enc_alt & 0xf) << 4 | (timesync<<3) | (ff<<2) | (evenenclat>>15)) - df_frame_even_bytes.append((evenenclat>>7) & 0xff) - df_frame_even_bytes.append(((evenenclat & 0x7f) << 1) | (evenenclon>>16)) - df_frame_even_bytes.append((evenenclon>>8) & 0xff) - df_frame_even_bytes.append((evenenclon ) & 0xff) + df_frame_even_bytes[4] = (tc<<3) | (ss<<1) | nicsb + df_frame_even_bytes[5] = (enc_alt>>4) & 0xff + df_frame_even_bytes[6] = (enc_alt & 0xf) << 4 | (timesync<<3) | (ff<<2) | (evenenclat>>15) + df_frame_even_bytes[7] = (evenenclat>>7) & 0xff + df_frame_even_bytes[8] = ((evenenclat & 0x7f) << 1) | (evenenclon>>16) + df_frame_even_bytes[9] = (evenenclon>>8) & 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 df_frame_odd_bytes = self.df_frame_start() # data - df_frame_odd_bytes.append((tc<<3) | (ss<<1) | nicsb) - df_frame_odd_bytes.append((enc_alt>>4) & 0xff) - df_frame_odd_bytes.append((enc_alt & 0xf) << 4 | (timesync<<3) | (ff<<2) | (oddenclat>>15)) - df_frame_odd_bytes.append((oddenclat>>7) & 0xff) - df_frame_odd_bytes.append(((oddenclat & 0x7f) << 1) | (oddenclon>>16)) - df_frame_odd_bytes.append((oddenclon>>8) & 0xff) - df_frame_odd_bytes.append((oddenclon ) & 0xff) + df_frame_odd_bytes[4] = (tc<<3) | (ss<<1) | nicsb + df_frame_odd_bytes[5] = (enc_alt>>4) & 0xff + df_frame_odd_bytes[6] = (enc_alt & 0xf) << 4 | (timesync<<3) | (ff<<2) | (oddenclat>>15) + df_frame_odd_bytes[7] = (oddenclat>>7) & 0xff + df_frame_odd_bytes[8] = ((oddenclat & 0x7f) << 1) | (oddenclon>>16) + df_frame_odd_bytes[9] = (oddenclon>>8) & 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) @@ -101,16 +118,17 @@ class ModeS: # Volume IV : Surveillance and Collision Avoidance Systems # Figure C-3. Extended Squitter Status # "Register 07_16" + #@Timed def df_encode_extended_squitter_status(self, trs = 0x0, ats = 0x0): df_frame = self.df_frame_start() - df_frame.append((trs << 6) & 0x3 | (ats << 5) & 0x1) - df_frame.extend([0]*6) + df_frame[4] = ((trs << 6) & 0x3 | (ats << 5) & 0x1) - self.df_frame_append_crc(df_frame) + self.df_frame_write_crc(df_frame) return df_frame #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): #1-5 downlink format @@ -161,15 +179,15 @@ class ModeS: dfvel = self.df_frame_start() # data - dfvel.append((tc << 3) | st) - dfvel.append((ic << 7) | (resv_a << 6) | (NAC << 3) | (S_EW << 2) | ((V_EW >> 8) & 0x03)) - dfvel.append(0xFF & V_EW) - dfvel.append((S_NS << 7) | ((V_NS >> 3))) #& 0x7F)) - dfvel.append(((V_NS << 5) & 0xE0) | (VrSrc << 4) | (S_Vr << 3) | ((Vr >> 6) & 0x03)) - dfvel.append(((Vr << 2) & 0xFC) | (RESV_B)) - dfvel.append((S_Dif << 7) | (Dif)) + dfvel[4] = ((tc << 3) | st) + dfvel[5] = ((ic << 7) | (resv_a << 6) | (NAC << 3) | (S_EW << 2) | ((V_EW >> 8) & 0x03)) + dfvel[6] = (0xFF & V_EW) + dfvel[7] = ((S_NS << 7) | ((V_NS >> 3))) #& 0x7F)) + dfvel[8] = (((V_NS << 5) & 0xE0) | (VrSrc << 4) | (S_Vr << 3) | ((Vr >> 6) & 0x03)) + dfvel[9] = (((Vr << 2) & 0xFC) | (RESV_B)) + dfvel[10] = ((S_Dif << 7) | (Dif)) - self.df_frame_append_crc(dfvel) + self.df_frame_write_crc(dfvel) return dfvel @@ -186,19 +204,17 @@ class ModeS: tc = 1 # §C.2.3.4 ec = 1 # §C.2.3.4 - map = "#ABCDEFGHIJKLMNOPQRSTUVWXYZ#####_###############0123456789######" - dfname = self.df_frame_start() # data - dfname.append((tc << 3) | (ec)) - dfname.append((0xFC & (int(map.find(csname[0])) << 2)) | (0x03 & (int(map.find(csname[1])) >> 6))) - dfname.append((0xF0 & (int(map.find(csname[1])) << 4)) | (0x0F & (int(map.find(csname[2])) >> 2))) - dfname.append((0xF0 & (int(map.find(csname[2])) << 6)) | (0x3F & (int(map.find(csname[3])) >> 0))) - dfname.append((0xFC & (int(map.find(csname[4])) << 2)) | (0x03 & (int(map.find(csname[5])) >> 4))) - dfname.append((0xF0 & (int(map.find(csname[5])) << 4)) | (0x0F & (int(map.find(csname[6])) >> 2))) - dfname.append((0xF0 & (int(map.find(csname[6])) << 6)) | (0x3F & (int(map.find(csname[7])) >> 0))) + dfname[4] = (tc << 3) | (ec) + dfname[5] = (0xFC & (int(self._charmap.find(csname[0])) << 2)) | (0x03 & (int(self._charmap.find(csname[1])) >> 4)) + dfname[6] = (0xF0 & (int(self._charmap.find(csname[1])) << 4)) | (0x0F & (int(self._charmap.find(csname[2])) >> 2)) + dfname[7] = (0xF0 & (int(self._charmap.find(csname[2])) << 6)) | (0x3F & (int(self._charmap.find(csname[3])) >> 0)) + dfname[8] = (0xFC & (int(self._charmap.find(csname[4])) << 2)) | (0x03 & (int(self._charmap.find(csname[5])) >> 4)) + dfname[9] = (0xF0 & (int(self._charmap.find(csname[5])) << 4)) | (0x0F & (int(self._charmap.find(csname[6])) >> 2)) + 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 @@ -208,6 +224,7 @@ class ModeS: # Figure C-8a. Extended Squitter Aircraft Status # "Register 61_16" + #@Timed def modaA_encode(self,modeA_4096_code = "7000", emergency_state = 0x0): frame = self.df_frame_start() # data @@ -216,7 +233,7 @@ class ModeS: # 1 : Emergency/Priority Status and Mode A Code # 2 : TCAS/ACAS RA Broadcast -> Figure C-8b : fields have different meaning # 3-7 : reserved - frame.append((format_tc << 3) | st) + frame[4] = ((format_tc << 3) | st) # Encode Squawk # ABCD (A:0-7, B:0-7, C:0-7, D:0-7) @@ -277,67 +294,39 @@ class ModeS: elif squawk_str == "7500": emergency = 0x5 - frame.append(emergency << 5 | squawk_bits >> 8) - frame.append(squawk_bits & 0xFF) + frame[5] = (emergency << 5 | squawk_bits >> 8) + frame[6] = (squawk_bits & 0xFF) - frame.extend([0]*4) - - self.df_frame_append_crc(frame) + self.df_frame_write_crc(frame) return frame -############################################################### -# Copyright (C) 2015 Junzi Sun (TU Delft) -# 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 . -############################################################### + #@Timed + def df_frame_write_crc(self,frame): + crc = self.crc24_lu(frame[0:11]) + frame[11] = crc >> 16 + frame[12] = (crc >> 8) & 0xFF + frame[13] = crc & 0xFF -# the polynominal generattor code for CRC - - def modes_crc(self, msg, encode=False): - """Mode-S Cyclic Redundancy Check - Detect if bit error occurs in the Mode-S message - Args: - msg (string): 28 bytes hexadecimal message string - encode (bool): True to encode the date only and return the checksum - Returns: - string: message checksum, or partity bits (encoder) - """ + #@Timed + # CRC24 without lookup table + def crc24(self,msg): + crc = 0x0 + for b in msg: + crc ^= (b << 16) + for i in range(8): + crc <<= 1 + if crc & 0x1000000: crc ^= self._crc_poly - GENERATOR = "1111111111111010000001001" # polynomial coefficients - - msgbin = list(self.hex2bin(msg)) + return crc & 0xFFFFFF - if encode: - msgbin[-24:] = ['0'] * 24 + #@Timed + # 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 - 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) + return crc & 0xFFFFFF diff --git a/ModeSLocation.py b/ModeSLocation.py index 1a3ac00..fa12148 100644 --- a/ModeSLocation.py +++ b/ModeSLocation.py @@ -1,4 +1,5 @@ import math +from CustomDecorators import * class ModeSLocation: """This class does ModeS/ADSB Location calulations""" @@ -89,6 +90,7 @@ class ModeSLocation: # encode CPR position # https://adsb-decode-guide.readthedocs.io/en/latest/content/cpr.html # compact position reporting + #@Timed def cpr_encode(self, lat, lon, ctype, surface): if surface is True: scalar = 2.**19 diff --git a/PseudoCircleTrajectorySimulator.py b/PseudoCircleTrajectorySimulator.py index 72013d9..695c019 100644 --- a/PseudoCircleTrajectorySimulator.py +++ b/PseudoCircleTrajectorySimulator.py @@ -20,32 +20,29 @@ from AbstractTrajectorySimulatorBase import AbstractTrajectorySimulatorBase class PseudoCircleTrajectorySimulator(AbstractTrajectorySimulatorBase): def __init__(self,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._lon0 = aircrafinfos.lon_deg def refresh_delay(self): - return 0.005 + return 0.02 def update_aircraftinfos(self): - now = datetime.datetime.now(datetime.timezone.utc) - elapsed = (now - self._lasttime).total_seconds() turn_rate = self.getTurnRate() R = self.getTurnRadius() 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) - 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.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): tr = (9.81/self._aircraftinfos.speed_mps)*math.sqrt(self._aircraftinfos.maxloadfactor**2.0 - 1.0) return tr def getTurnRadius(self): - return self._aircraftinfos.speed_mps/self.getTurnRate() \ No newline at end of file + return self._aircraftinfos.speed_mps/self.getTurnRate() diff --git a/README.md b/README.md index cbe2651..1b14ae5 100755 --- a/README.md +++ b/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. 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 : - 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 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 Scenario mode with a provided scenario filepath +--icao Callsign in hex, Default:0x508035 +--callsign Callsign (8 chars max), Default:DEADBEEF +--squawk 4-digits 4096 code squawk, Default:7000 +--trajectorytype 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 Latitude for the plane in decimal degrees, Default:50.44994 +--long Longitude for the place in decimal degrees. Default:30.5211 +--altitude Altitude in decimal feet, Default:1500.0 +--speed Airspeed in decimal kph, Default:300.0 +--vspeed Vertical speed en ft/min, positive up, Default:0 +--maxloadfactor Specify the max load factor for aircraft simulation. Default:1.45 +--trackangle Track angle in decimal degrees. Default:0 +--timesync 0/1, 0 indicates time not synchronous with UTC, Default:0 +--capability Capability, Default:5 +--typecode ADS-B message type, Default:11 +--sstatus Surveillance status, Default:0 +--nicsupplementb NIC supplement-B, Default:0 +--surface Aircraft located on ground, Default:False +--waypoints Waypoints file for waypoints trajectory +--posrate 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` 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` @@ -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 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 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: - 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](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): [DEFCON 20 (2012) - RenderMan - Hacker + Airplanes = No Good Can Come Of This](https://www.youtube.com/watch?v=mY2uiLfXmaI) diff --git a/examples/A400M-094.json b/examples/A400M-094.json new file mode 100644 index 0000000..56aa0dd --- /dev/null +++ b/examples/A400M-094.json @@ -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 +} \ No newline at end of file diff --git a/examples/A400M-110.json b/examples/A400M-110.json new file mode 100644 index 0000000..603eff6 --- /dev/null +++ b/examples/A400M-110.json @@ -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 +} \ No newline at end of file diff --git a/examples/AN-IL78MP.json b/examples/AN-IL78MP.json new file mode 100644 index 0000000..3e8e50d --- /dev/null +++ b/examples/AN-IL78MP.json @@ -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 +} \ No newline at end of file diff --git a/examples/MRYIA.json b/examples/MRYIA.json new file mode 100644 index 0000000..2f3638e --- /dev/null +++ b/examples/MRYIA.json @@ -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 +} \ No newline at end of file diff --git a/examples/scenario.json b/examples/scenario.json new file mode 100644 index 0000000..33ce0c6 --- /dev/null +++ b/examples/scenario.json @@ -0,0 +1,7 @@ +{ + "plane1": + { + "filename":"./examples/MRYIA.json", + "trajectory_type":"circle" + } +} diff --git a/examples/scenario2.json b/examples/scenario2.json new file mode 100644 index 0000000..fc98a0b --- /dev/null +++ b/examples/scenario2.json @@ -0,0 +1,12 @@ +{ + "plane1": + { + "filename":"./examples/MRYIA.json", + "trajectory_type":"circle" + }, + "plane2": + { + "filename":"./examples/AN-IL78MP.json", + "trajectory_type":"circle" + } +} diff --git a/examples/scenario3.json b/examples/scenario3.json new file mode 100644 index 0000000..11bbefa --- /dev/null +++ b/examples/scenario3.json @@ -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" + } +} diff --git a/adsb-out-circle.png b/images/adsb-out-circle.png similarity index 100% rename from adsb-out-circle.png rename to images/adsb-out-circle.png diff --git a/adsb-out-random.png b/images/adsb-out-random.png similarity index 100% rename from adsb-out-random.png rename to images/adsb-out-random.png diff --git a/images/adsb-out-scenario3.png b/images/adsb-out-scenario3.png new file mode 100644 index 0000000..a1be8bb Binary files /dev/null and b/images/adsb-out-scenario3.png differ diff --git a/test-setup.jpg b/images/test-setup.jpg similarity index 100% rename from test-setup.jpg rename to images/test-setup.jpg diff --git a/realtime-adsb-out.py b/realtime-adsb-out.py index 0a5fc50..708df6e 100755 --- a/realtime-adsb-out.py +++ b/realtime-adsb-out.py @@ -14,8 +14,9 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . """ -import sys, time, math -import threading +import sys, time, math, os +import threading, json +import traceback from AircraftInfos import AircraftInfos from FixedTrajectorySimulator import FixedTrajectorySimulator @@ -30,6 +31,7 @@ def usage(msg=False): if msg:print(msg) print("Usage: %s [options]\n" % sys.argv[0]) print("-h | --help Display help message.") + print("--scenario Scenario mode with a provided scenario filepath") print("--icao Callsign in hex, Default:0x508035") print("--callsign Callsign (8 chars max), Default:DEADBEEF") print("--squawk 4-digits 4096 code squawk, Default:7000") @@ -59,6 +61,21 @@ def usage(msg=False): 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(): # Default values @@ -82,10 +99,10 @@ def main(): trajectory_type = 'fixed' waypoints_file = None posrate = 150000 - + scenariofile = None try: (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=' ]) except GetoptError as err: @@ -94,6 +111,7 @@ def main(): if len(opts) != 0: for (opt, arg) in opts: if opt in ('-h', '--help'):usage() + elif opt in ('--scenario'):scenariofile = arg elif opt in ('--icao'):icao_aa = arg elif opt in ('--callsign'):callsign = arg elif opt in ('--squawk'):squawk = arg @@ -117,38 +135,64 @@ def main(): elif opt in ('--posrate'):posrate = int(arg) else:usage("Unknown option %s\n" % opt) - 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) + track_simulators = [] + broadcast_thread = HackRfBroadcastThread(posrate) # posrate would usally be used with random mode to generate load of tracks - # TODO : the mutex would better be implemented as an object attribute in broadcast thread - mutex = threading.Lock() + if scenariofile == None: + 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': - trajectory_simulator = FixedTrajectorySimulator(mutex,brodcast_thread,aircraftinfos) - elif trajectory_type == 'circle': - trajectory_simulator = PseudoCircleTrajectorySimulator(mutex,brodcast_thread,aircraftinfos) - elif trajectory_type == 'random': - trajectory_simulator = RandomTrajectorySimulator(mutex,brodcast_thread,aircraftinfos) - elif trajectory_type == 'waypoints': - print("WaypointsTrajectorySimulator not implemented yet") - exit(-1) - trajectory_simulator = WaypointsTrajectorySimulator(mutex,brodcast_thread,aircraftinfos,waypoints_file) + track_simulators.append(track_simulation) + + else: + print("Going to run in json scenario mode from file "+os.path.abspath(scenariofile)) + with open(scenariofile,'r') as json_file: + scenario = json.load(json_file) + + for plane in scenario.values(): + plane_info = AircraftInfos.from_json(plane["filename"]) + + 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'): time.sleep(0.05) - trajectory_simulator.start() - brodcast_thread.start() + # start all threads + 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) while(val:=input("") != 's'): time.sleep(0.05) - trajectory_simulator.stop() - brodcast_thread.stop() - trajectory_simulator.join() - brodcast_thread.join() + + # stop all threads + for tsim in track_simulators: + 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")