initial commit

This commit is contained in:
Mathieu Peyréga 2022-03-10 21:25:48 +01:00
parent 92880173c3
commit ffd073f1fd
17 changed files with 2581 additions and 0 deletions

102
ADSBLowLevelEncoder.py Normal file
View File

@ -0,0 +1,102 @@
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
###############################################################
# Further work on fork
# Copyright (C) 2017 David Robinson
def extract_bit(self, byte, pos):
"""
Extract a bit from a given byte using MS ordering.
ie. B7 B6 B5 B4 B3 B2 B1 B0
"""
return (byte >> pos) & 0x01
def manchester_encode(self, byte):
"""
Encode a byte using Manchester encoding. Returns an array of bits.
Adds two start bits (1, 1) and one stop bit (0) to the array.
"""
manchester_encoded = []
# 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])
return manchester_encoded
def frame_1090es_ppm_modulate(self, even, odd = []):
"""
Args:
even and odd: The data frames that need to be converted to PPM
Returns:
The bytearray of the PPM data
"""
ppm = [ ]
length_even = len(even)
length_odd = len(odd)
if (length_even != 0):
ppm.extend(self.adsb_frame_pause) # pause
ppm.extend(self.adsb_frame_preamble) # preamble
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
if (length_odd != 0):
ppm.extend(self.adsb_frame_pause) # pause
ppm.extend(self.adsb_frame_preamble) # preamble
for i in range(length_odd):
word16 = numpy.packbits(self.manchester_encode(~odd[i]))
ppm.extend(word16[0:2])
ppm.extend(self.adsb_frame_pause) # pause
return bytearray(ppm)
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)
return bytearray(signal)

View File

@ -0,0 +1,97 @@
""" Abstract base class for a trajectory simulation
This class provides basic services that will generate and feed broadcasting
thread with appropriate messages.
2 abstract methods need to be overriden in derived classes :
- refresh_delay which should return the simulation timestep in seconds
- update_aircraftinfos which is reponsible for animating the aircraftinfos
object at each time step, thus making the simulation "alive"
mutex protection occurs when calling replace_message
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/>.
"""
import time
import abc
import threading
from ModeS import ModeS
class AbstractTrajectorySimulatorBase(threading.Thread,abc.ABC):
def __init__(self,mutex,broadcast_thread,aircraftinfos):
super().__init__()
self._mutex = mutex
self._broadcast_thread = broadcast_thread
self._aircraftinfos = aircraftinfos
self._modeSencoder = ModeS(df=17,icao=self._aircraftinfos.icao,ca=self._aircraftinfos.capability)
self._do_stop = False
# 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
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)
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)
# 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.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)
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
# 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):
...

291
AircraftInfos.py Normal file
View File

@ -0,0 +1,291 @@
""" This class holds the aircraft states from the ADS-B point of view
It is refreshed by the simulation thread (or sensor feed thread) and will
be used to provide broadcasted informations
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/>.
"""
import math
class AircraftInfos:
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):
self._icao = int(icao,16)
self._oldicao = self._icao
self._callsign = callsign
self._oldcallsign = self._callsign
self._squawk = squawk
self._oldsquawk = self._squawk
self._lat_deg = float(lat_deg)
self._oldlat_deg = self._lat_deg
self._lon_deg = float(lon_deg)
self._oldlon_deg = self._lon_deg
self._alt_msl_m = float(alt_msl_ft)*0.3048
self._oldalt_msl_m = self._alt_msl_m
self._speed_mps = float(speed_kph)/3.6
self._oldspeed_mps = self._speed_mps
self._vspeed_mps = float(vspeed_ftpmin)*0.00508
self._oldvspeed_mps = self._vspeed_mps
self._maxloadfactor = float(maxloadfactor)
self._oldmaxloadfactor = self._maxloadfactor
self._track_angle_deg = math.fmod(float(track_angle_deg),360.0)
self._oldtrack_angle_deg = self._track_angle_deg
self._timesync = int(timesync)
self._oldtimesync = self._timesync
self._capability = int(capability)
self._oldcapability = self._capability
self._type_code = int(type_code)
self._oldtype_code = self._type_code
self._surveillance_status = int(surveillance_status)
self._oldsurveillance_status = self._surveillance_status
self._nicsupb = int(nicsupb)
self._oldnicsupb = self._nicsupb
self._on_surface = on_surface
self._oldon_surface = self._on_surface
################################################
@property
def icao(self):
return self._icao
@icao.setter
def icao(self,value):
self._oldicao = self._icao
self._icao = value
@property
def icao_changed(self):
return self._icao != self._oldicao
################################################
@property
def callsign(self):
return self._callsign
@callsign.setter
def callsign(self,value):
self._oldcallsign = self._callsign
self._callsign = value
@property
def callsign_changed(self):
return self._callsign != self._oldcallsign
################################################
@property
def squawk(self):
return self._squawk
@squawk.setter
def squawk(self,value):
self._oldsquawk = self._squawk
self._squawk = value
@property
def squawk_changed(self):
return self._squawk != self._oldsquawk
################################################
@property
def lat_deg(self):
return self._lat_deg
@lat_deg.setter
def lat_deg(self,value):
self._oldlat_deg = self._lat_deg
self._lat_deg = value
@property
def lat_changed(self):
return self._lat_deg != self._oldlat_deg
################################################
@property
def lon_deg(self):
return self._lon_deg
@lon_deg.setter
def lon_deg(self,value):
self._oldlon_deg = self._lon_deg
self._lon_deg = value
@property
def lon_changed(self):
return self._lon_deg != self._oldlon_deg
################################################
@property
def alt_msl_m(self):
return self._alt_msl_m
@property
def alt_msl_ft(self):
return self._alt_msl_m / 0.3048
@alt_msl_m.setter
def alt_msl_m(self,value):
self._oldalt_msl_m = self._alt_msl_m
self._alt_msl_m = value
@property
def alt_msl_changed(self):
return self._alt_msl_m != self._oldalt_msl_m
################################################
@property
def speed_mps(self):
return self._speed_mps
@property
def speed_kt(self):
return self._speed_mps*1.94384449244
@speed_mps.setter
def speed_mps(self,value):
self._oldspeed_mps != self._speed_mps
self._speed_mps = value
@property
def speed_changed(self):
return self._speed_mps != self._oldspeed_mps
################################################
@property
def vspeed_mps(self):
return self._vspeed_mps
@property
def vspeed_ftpmin(self):
return self._vspeed_mps * 196.850393701
@vspeed_mps.setter
def vspeed_mps(self,value):
self._oldvspeed_mps = self._vspeed_mps
self._vspeed_mps = value
@property
def vspeed_changed(self):
return self._vspeed_mps != self._oldvspeed_mps
################################################
@property
def maxloadfactor(self):
return self._maxloadfactor
@maxloadfactor.setter
def maxloadfactor(self,value):
self._oldmaxloadfactor = self._maxloadfactor
self._maxloadfactor = value
@property
def maxloadfactor_changed(self):
return self._maxloadfactor != self._oldmaxloadfactor
################################################
@property
def track_angle_deg(self):
return self._track_angle_deg
@track_angle_deg.setter
def track_angle_deg(self,value):
self._oldtrack_angle_deg = self._track_angle_deg
self._track_angle_deg = value
@property
def track_angle_changed(self):
return self._track_angle_deg != self._oldtrack_angle_deg
################################################
@property
def timesync(self):
return self._timesync
@timesync.setter
def timesync(self,value):
self._oldtimesync = self._timesync
self._timesync = value
@property
def timesync_changed(self):
return self._timesync != self._oldtimesync
################################################
@property
def capability(self):
return self._capability
@capability.setter
def capability(self,value):
self._oldcapability = self._capability
self._capability = value
@property
def capability_changed(self):
return self._capability != self._oldcapability
################################################
@property
def type_code(self):
return self._type_code
@type_code.setter
def type_code(self,value):
self._oldtype_code = self._type_code
self._type_code = value
@property
def type_code_changed(self):
return self._type_code != self._oldtype_code
################################################
@property
def surveillance_status(self):
return self._surveillance_status
@surveillance_status.setter
def surveillance_status(self,value):
self._oldsurveillance_status = self._surveillance_status
self._surveillance_status = value
@property
def surveillance_status_changed(self):
return self._surveillance_status != self._oldsurveillance_status
################################################
@property
def nicsupb(self):
return self._nicsupb
@nicsupb.setter
def nicsupb(self,value):
self._oldnicsupb = self._nicsupb
self._nicsupb = value
@property
def nicsupb_changed(self):
return self._nicsupb != self._oldnicsupb
################################################
@property
def on_surface(self):
return self._on_surface
@on_surface.setter
def on_surface(self,value):
self._oldon_surface = self._on_surface
self._on_surface = value
@property
def on_surface_changed(self):
return self._on_surface != self._oldon_surface

22
CustomDecorators.py Normal file
View File

@ -0,0 +1,22 @@
import time
def Singleton(class_):
instances = {}
def getinstance(*args, **kwargs):
if class_ not in instances:
instances[class_] = class_(*args, **kwargs)
return instances[class_]
return getinstance
def Timed(method):
def timed(*args, **kw):
ts = time.time()
result = method(*args, **kw)
te = time.time()
if 'log_time' in kw:
name = kw.get('log_name', method.__name__.upper())
kw['log_time'][name] = int((te - ts) * 1000)
else:
print('%r total execution time was %2.2f ms' % (method.__name__, (te - ts) * 1000))
return result
return timed

View File

@ -0,0 +1,27 @@
""" simplest implementation of a trajectory simulation where the simulated
aircraft is steady at the provided position
mutex protection occurs when calling replace_message
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 AbstractTrajectorySimulatorBase import AbstractTrajectorySimulatorBase
class FixedTrajectorySimulator(AbstractTrajectorySimulatorBase):
def __init__(self,mutex,broadcast_thread,aircrafinfos):
super().__init__(mutex,broadcast_thread,aircrafinfos)
def refresh_delay(self):
return 0.005
def update_aircraftinfos(self):
pass

177
HackRfBroadcastThread.py Normal file
View File

@ -0,0 +1,177 @@
""" This class holds the aircraft states from the ADS-B point of view
It is refreshed by the simulation thread (or sensor feed thread) and will
be used to provide broadcasted informations
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/>.
"""
#
# This class overrides threading.Thread and provides service to broacast
# ADS-B message though a HackRF device
# message updates are performed from a separate thread which will
# update/push messages thanks to the replace_message method
# thread loop will pump and broacast updated message (soft realtime)
#
# mutex protection mecanism is implemented in
# replace_message() which is call from other thread
# broadcast_one_message() which is called from this thread
# in order to prevent concurrent access to broadcasted data buffers
import time, datetime, math
import threading
from CustomDecorators import *
from ADSBLowLevelEncoder import ADSBLowLevelEncoder
from pyhackrf import *
from ctypes import *
class hackrf_tx_context(Structure):
_fields_ = [("buffer", POINTER(c_ubyte)),
("last_tx_pos", c_int),
("buffer_length", c_int) ]
def hackrfTXCB(hackrf_transfer):
user_tx_context = cast(hackrf_transfer.contents.tx_ctx, POINTER(hackrf_tx_context))
tx_buffer_length = hackrf_transfer.contents.valid_length
left = user_tx_context.contents.buffer_length - user_tx_context.contents.last_tx_pos
addr_dest = addressof(hackrf_transfer.contents.buffer.contents)
addr_src = addressof(user_tx_context.contents.buffer.contents)
if (left > tx_buffer_length):
memmove(addr_dest,addr_src,tx_buffer_length)
user_tx_context.contents.last_tx_pos += tx_buffer_length
return 0
else:
memmove(addr_dest,addr_src,left)
memset(addr_dest+left,0,tx_buffer_length-left)
return -1
@Singleton
class HackRfBroadcastThread(threading.Thread):
def __init__(self,mutex,airborne_position_refresh_period = 150000):
super().__init__()
self._mutex = mutex
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
# Initialize pyHackRF library
result = HackRF.initialize()
if (result != LibHackRfReturnCode.HACKRF_SUCCESS):
print("Error :",result, ",", HackRF.getHackRfErrorCodeName(result))
# Initialize HackRF instance (could pass board serial or index if specific board is needed)
self._hackrf_broadcaster = HackRF()
# Do requiered settings
# so far hard-coded e.g. gain and disabled amp are specific to hardware test setup
# with hackrf feeding a flight aware dongle through cable + attenuators (-50dB)
result = self._hackrf_broadcaster.open()
if (result != LibHackRfReturnCode.HACKRF_SUCCESS):
print("Error :",result, ",", HackRF.getHackRfErrorCodeName(result))
result = self._hackrf_broadcaster.setSampleRate(2000000)
if (result != LibHackRfReturnCode.HACKRF_SUCCESS):
print("Error :",result, ",", HackRF.getHackRfErrorCodeName(result))
result = self._hackrf_broadcaster.setBasebandFilterBandwidth(HackRF.computeBaseBandFilterBw(2000000))
if (result != LibHackRfReturnCode.HACKRF_SUCCESS):
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
# only if you use wire feed (you'll need attenuators in that case)
if (result != LibHackRfReturnCode.HACKRF_SUCCESS):
print("Error :",result, ",", HackRF.getHackRfErrorCodeName(result))
result = self._hackrf_broadcaster.setTXVGAGain(4) # week gain (used for wire feed + attenuators)
if (result != LibHackRfReturnCode.HACKRF_SUCCESS):
print("Error :",result, ",", HackRF.getHackRfErrorCodeName(result))
result = self._hackrf_broadcaster.setAmplifierMode(LibHackRfHwMode.HW_MODE_OFF)
if (result != LibHackRfReturnCode.HACKRF_SUCCESS):
print("Error :",result, ",", HackRF.getHackRfErrorCodeName(result))
self._tx_context = hackrf_tx_context()
self._do_stop = False
# do hackRF lib and instance cleanup at object destruction time
def __del__(self):
result = self._hackrf_broadcaster.close()
if (result != LibHackRfReturnCode.HACKRF_SUCCESS):
print("Error :",result, ",", HackRF.getHackRfErrorCodeName(result))
result = HackRF.deinitialize()
if (result != LibHackRfReturnCode.HACKRF_SUCCESS):
print("Error :",result, ",", HackRF.getHackRfErrorCodeName(result))
def stop(self):
self._do_stop = True
# updates the next data to be broadcaster for a given message type
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
self._mutex.acquire()
self._messages[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
self._mutex.release()
result = self._hackrf_broadcaster.startTX(hackrfTXCB,self._tx_context)
if (result != LibHackRfReturnCode.HACKRF_SUCCESS):
print("Error :",result, ",", HackRF.getHackRfErrorCodeName(result))
while self._hackrf_broadcaster.isStreaming():
time.sleep(0.00001)
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
# upon exit, reset _do_stop flag in case there is a new start
self._do_stop = False

343
ModeS.py Normal file
View File

@ -0,0 +1,343 @@
from ModeSLocation import ModeSLocation
import math
import numpy
###############################################################
# Further work on fork
# Copyright (C) 2017 David Robinson
class ModeS:
"""This class handles the ModeS ADSB manipulation
"""
def __init__(self,df,icao,ca):
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)
self.icao = icao # 24 bits icao registration code
self.ca = ca # capability see §3.1.2.5.2.2.1
# (will usually be 5 for level 2 transponder and airborne)
def df_frame_start(self):
"""
This will build the usual df frame start
"""
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)
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"
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
tc = type code (§C2.3.1)
ss = surveillance status : 0 = no condition information
1 = permanent alert (emergency condition)
2 = temporary alert (change in Mode A identity code other than emergency condition)
3 = SPI condition
nicsb = NIC supplement-B (§C.2.3.2.5)
"""
location = ModeSLocation()
enc_alt = 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)
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)
self.df_frame_append_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)
self.df_frame_append_crc(df_frame_odd_bytes)
return (df_frame_even_bytes, df_frame_odd_bytes)
# Ref :
# ICAO Annex 10 : Aeronautical Telecommunications
# Volume IV : Surveillance and Collision Avoidance Systems
# Figure C-1. Extended Squitter Surface Position
# "Register 06_16"
def df_encode_surface_position(self, lat, lon, alt, tc, ss, nicsb, timesync):
# TODO
exit(-1)
# Ref :
# ICAO Annex 10 : Aeronautical Telecommunications
# Volume IV : Surveillance and Collision Avoidance Systems
# Figure C-3. Extended Squitter Status
# "Register 07_16"
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)
self.df_frame_append_crc(df_frame)
return df_frame
#From https://github.com/jaywilhelm/ADSB-Out_Python on 2019-08-18
def df_encode_ground_velocity(self, ground_velocity_kt, track_angle_deg, vertical_rate):
#1-5 downlink format
#6-8 CA capability
#9-32 ICAO
#33-88 DATA -> 33-87 w/ 33-37 TC
#89-112 Parity
track_angle_rad = numpy.deg2rad(track_angle_deg)
V_EW = ground_velocity_kt*numpy.sin(track_angle_rad)
V_NS = ground_velocity_kt*numpy.cos(track_angle_rad)
if(V_EW >= 0):
S_EW = 0
else:
S_EW = 1
if(V_NS >= 0):
S_NS = 0
else:
S_NS = 1
V_EW = int(abs(V_EW))+1
V_NS = int(abs(V_NS))+1
S_Vr = 0
Vr = int(vertical_rate)+1
if(vertical_rate < 0):
Vr = -Vr
S_Vr = 1
tc = 19 #33-37 1-5 type code
st = 0x01 #38-40 6-8 subtype, 3 air, 1 ground speed
ic = 0 # #41 9 intent change flag
resv_a = 0#1 #42 10
NAC = 2#0 #43-45 11-13 velocity uncertainty
#S_EW = 1#1 #46 14
#V_EW = 97#9 #47-56 15-24
#S_NS = 0#1 #57 25 north-south sign
#V_NS = 379#0xA0 #58-67 26-35 160 north-south vel
VrSrc = 1#0 #68 36 vertical rate source
#S_Vr = 1#1 #69 37 vertical rate sign
#Vr = 41#0x0E #70-78 38-46 14 vertical rate
RESV_B = 0 #79-80 47-48
S_Dif = 0 #81 49 diff from baro alt, sign
Dif = 0x1c#0x17 #82-88 50-66 23 diff from baro alt
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))
self.df_frame_append_crc(dfvel)
return dfvel
#From https://github.com/jaywilhelm/ADSB-Out_Python on 2019-08-25
# TODO the callsign must be 8
def callsign_encode(self, csname):
#Pad the callsign to be 8 characters
csname = csname.ljust(8, '_')
if len(csname) > 8 or len(csname) <= 0:
print ("Name length error")
return None
csname = csname.upper()
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)))
self.df_frame_append_crc(dfname)
return dfname
# Ref :
# ICAO Annex 10 : Aeronautical Telecommunications
# Volume IV : Surveillance and Collision Avoidance Systems
# Figure C-8a. Extended Squitter Aircraft Status
# "Register 61_16"
def modaA_encode(self,modeA_4096_code = "7000", emergency_state = 0x0):
frame = self.df_frame_start()
# data
format_tc = 28
st = 0x01 # 0 : No information
# 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)
# Encode Squawk
# ABCD (A:0-7, B:0-7, C:0-7, D:0-7)
# A = a4,a2,a1
# B = b4,b2,b1
# C = c4,c2,c1
# D = d4,d2,d1
# bits = c1,a1,c2,a2,c4,a4,0,b1,d1,b2,d2,b4,d4
if isinstance(modeA_4096_code,int):
squawk_str = '{:04d}'.format(modeA_4096_code)
elif isinstance(modeA_4096_code,str):
squawk_str = modeA_4096_code
else:
print("squawk must be provided as decimal int or 4 digits string")
exit(-1)
if (len(squawk_str) == 4):
test_digits = True
for i in range(4):
test_digits = test_digits and (squawk_str[i] >= '0' and squawk_str[i] <= '7')
if not test_digits:
print("all 4 squawk digits must be in 0-7 range")
exit(-1)
else:
print("squawk must be 4 digits string")
exit(-1)
a = "{0:03b}".format(int(squawk_str[0]))
b = "{0:03b}".format(int(squawk_str[1]))
c = "{0:03b}".format(int(squawk_str[2]))
d = "{0:03b}".format(int(squawk_str[3]))
a4 = int(a[0])
a2 = int(a[1])
a1 = int(a[2])
b4 = int(b[0])
b2 = int(b[1])
b1 = int(b[2])
c4 = int(c[0])
c2 = int(c[1])
c1 = int(c[2])
d4 = int(d[0])
d2 = int(d[1])
d1 = int(d[2])
squawk_bits = d4 | b4 << 1 | d2 << 2 | b2 << 3 | d1 << 4 | b1 << 5 | a4 << 7 | c4 << 8 | a2 << 9 | c2 << 10 | a1 << 11 | c1 << 12
emergency = emergency_state
if squawk_str == "7700":
emergency = 0x1
elif squawk_str == "7600":
emergency = 0x4
elif squawk_str == "7500":
emergency = 0x5
frame.append(emergency << 5 | squawk_bits >> 8)
frame.append(squawk_bits & 0xFF)
frame.extend([0]*4)
self.df_frame_append_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 <http://www.gnu.org/licenses/>.
###############################################################
# 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)
"""
GENERATOR = "1111111111111010000001001" # polynomial coefficients
msgbin = list(self.hex2bin(msg))
if encode:
msgbin[-24:] = ['0'] * 24
# 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)

110
ModeSLocation.py Normal file
View File

@ -0,0 +1,110 @@
import math
class ModeSLocation:
"""This class does ModeS/ADSB Location calulations"""
def __init__(self):
self.latz = 15
##########################################################################
# Copyright 2010, 2012 Nick Foster
# # This file is part of gr-air-modes
#
# gr-air-modes 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, or (at your option)
# any later version.
#
# gr-air-modes 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 gr-air-modes; see the file COPYING. If not, write to
# the Free Software Foundation, Inc., 51 Franklin Street,
# Boston, MA 02110-1301, USA.
#
###############################################################
# Further work on fork
# Copyright (C) 2017 David Robinson
def encode_alt_modes(self, alt, bit13):
# need to better understand as the >50175 feet not working
mbit = False
qbit = True
# For altitudes -1000<=X<=50175 feet, set bit 8 AKA the Q bit to true which means 25 feet resoulution
# For >50175 set the qbit to False and use 100 feet resoultion
if alt > 50175:
qbit = False
encalt = int((int(alt) + 1000) / 100)
else:
qbit = True
encalt = int((int(alt) + 1000) / 25)
if bit13 is True:
tmp1 = (encalt & 0xfe0) << 2
tmp2 = (encalt & 0x010) << 1
else:
tmp1 = (encalt & 0xff8) << 1
tmp2 = 0
return (encalt & 0x0F) | tmp1 | tmp2 | (mbit << 6) | (qbit << 4)
def nz(self, ctype):
"""
Number of geographic latitude zones between equator and a pole. It is set to NZ = 15 for Mode-S CPR encoding
https://adsb-decode-guide.readthedocs.io/en/latest/content/cpr.html
"""
return 4 * self.latz - ctype
def dlat(self, ctype, surface):
if surface == 1:
tmp = 90.0
else:
tmp = 360.0
nzcalc = self.nz(ctype)
if nzcalc == 0:
return tmp
else:
return tmp / nzcalc
def nl(self, declat_in):
if abs(declat_in) >= 87.0:
return 1.0
return math.floor( (2.0*math.pi) / math.acos(1.0- (1.0-math.cos(math.pi/(2.0*self.latz))) / math.cos( (math.pi/180.0)*abs(declat_in) )**2 ))
def dlon(self, declat_in, ctype, surface):
if surface:
tmp = 90.0
else:
tmp = 360.0
nlcalc = max(self.nl(declat_in)-ctype, 1)
return tmp / nlcalc
# encode CPR position
# https://adsb-decode-guide.readthedocs.io/en/latest/content/cpr.html
# compact position reporting
def cpr_encode(self, lat, lon, ctype, surface):
if surface is True:
scalar = 2.**19
else:
scalar = 2.**17
#encode using 360 constant for segment size.
dlati = self.dlat(ctype, False)
yz = math.floor(scalar * ((lat % dlati)/dlati) + 0.5)
rlat = dlati * ((yz / scalar) + math.floor(lat / dlati))
#encode using 360 constant for segment size.
dloni = self.dlon(lat, ctype, False)
xz = math.floor(scalar * ((lon % dloni)/dloni) + 0.5)
yz = int(yz) & (2**17-1)
xz = int(xz) & (2**17-1)
return (yz, xz) #lat, lon

View File

@ -0,0 +1,51 @@
""" simplest implementation of a trajectory simulation where the simulated
aircraft is flying a pseudo circle around center position at max load factor
mutex protection occurs when calling replace_message
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/>.
"""
import datetime, math
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
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 = math.fmod(ta,360.0)
self._aircraftinfos.track_angle_deg = ta
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
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()

100
README.md Executable file
View File

@ -0,0 +1,100 @@
# realtime ADS-B out
## Foreword
This project is inspired and reuse several parts of several other ADS-B / mode S projects amongst which:
- https://github.com/lyusupov/ADSB-Out
- https://github.com/nzkarit/ADSB-Out and https://github.com/pynstrom/adsb-out
- https://github.com/bistromath/gr-air-modes
- https://github.com/junzis/pyModeS
All those repositories are published under GNU General Public License v3.0. This is also the license chosen for this repository.
Please let me know if you have issues or require more explicit citations about reused source code.
## Project goals
The initial project goals are oriented towards:
- completing the set of broadcastable messages that have already been implemented "adsb-out" in referenced projects.
- fixing bugs / adding features in existing code.
- producing a software architecture that better suit my understanding/habits.
- beeing able to live feed HackRF through a libhackrf python wrapper layer, rather than generating an IQ sample files that would later be hackrf_transfer'd.
## HackRF python wrapper
HackRF python wrapper `pyhackrf.py` is included in this repository but is also proposed to be merged into hackRF main repository: https://github.com/greatscottgadgets/hackrf/pull/1058
If the pull request get accepted, file `pyhackrf.py` will be removed from this repo.
This repo only uses TX feature of the python wrapper, but RX is also possible (see examples in the PR)
At time of writting this guide, I also believe there is a regression in `libhackrf` which should be solved by PR: https://github.com/greatscottgadgets/hackrf/pull/1057
This is still under review from greatscottgadgets maintainers but code in this repo is tested with the PR included.
I have not tested it with older/officiel releases of hackrf drivers/dev lib versions.
## Software architecture
The workflow is divided between 3 execution threads:
- main thread wich performs all initializations and control user inputs (mainly start / stop simulation for now)
- hackrf broadcasting thread which pump encoded messages and send them over the air with a predefined schedule
- trajectory simulation thread which feed brodcasting thread with encoded messages matching a real time simulated trajectory
The message encoding is splitted into mode S "frame encoding" and "low level encoding" which handles PPM modulation and conversion to hackRF IQ sample format.
Software source code structure tries to reflect those 2 different layers.
So far only "simple" simulated trajectories are available, but one can easily extend/fork behaviour to e.g. have a flight informations coming from a flight simulator (X-plane would be pretty well suited for that purpose through it's UDP aircraft state broadcast facility) or use actual sensors to feed live data.
## Usage and RF broadcast disclaimer
Usage can be demonstrated together with `dump1090-mutability` or `dump1090-fa` and associated webservers or text message views.
Repository source code is tuned for a 1090 MHz brodcast with **direct wire feed** to a receiver SDR dongle (no over the air broadcast).
The hardware setup I'm using is pictured below. Please note the RF attenuators (-20dB and -30dB).
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")
The default hackrf settings in repo are :
- 1090 MHz
- LNA amplificator disabled
- TX gain 4dB
- Sample rate needs to be 2MHz as this matches the ADS-B specification where PPM symbols last for 0.5 µs.
Actual ADS-B brodcast frequency is 1090MHz which in most if not all places is a reserved band.
Some critical **flight safety feature** do rely on actual ADS-B broadcasts.
Unless you have special authorisations, **you should NEVER broadcast over the air at this frequency**.
If you can't use a wired RF feeding between hackRF and your SDR receiver for your test setup, you can easily modify source code in order to use a "fake" free frequency (e.g. 868MHz) and setup dump1090 accordingly to match this "fake" frequency by adding switch `--freq 868000000` to your usual `dump1090` command line. Increasing TX gain may be needed in that use case.
By the way, I believe that the fact that one with 200$ hardware would actually be able to broadcast at 1090MHz and produce some fake ADS-B aircraft tracks highlights a serious weakness in ADS-B design.
Those forged broadcasts may be used to spoof ATC, trigger TCAS or other malicious behaviours.
## Command line examples
`./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")
`./realtime-adsb-out.py --callsign 'FCKPUTIN' --alt 4500 --trajectorytype random`
will generate a random trajectory in a ~30s at specified (here default) speed around center lat / lon (default here too).
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")
## Reference documentation
All reference documentation from the repositories mentionned in the foreword.
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
- french version https://www.bazl.admin.ch/bazl/fr/home/experts/reglementation-et-informations-de-base/bases-legales-et-directives/annexes-a-la-convention-de-l-organisation-internationale-de-l-av.html
*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)

View File

@ -0,0 +1,45 @@
""" simplest implementation of a trajectory simulation where the simulated
aircraft is randomly distributed inside a circle
mutex protection occurs when calling replace_message
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/>.
"""
import random
import datetime, math
from AbstractTrajectorySimulatorBase import AbstractTrajectorySimulatorBase
class RandomTrajectorySimulator(AbstractTrajectorySimulatorBase):
def __init__(self,mutex,broadcast_thread,aircrafinfos):
super().__init__(mutex,broadcast_thread,aircrafinfos)
self._starttime = datetime.datetime.now(datetime.timezone.utc)
self._lat0 = aircrafinfos.lat_deg
self._lon0 = aircrafinfos.lon_deg
self._max_alt_m = aircrafinfos.alt_msl_m
self._max_speed_mps = aircrafinfos.speed_mps
def refresh_delay(self):
return 0.005
def update_aircraftinfos(self):
d0 = self._max_speed_mps * 30.0
Rlat = (d0/6371000.0)*(180.0/math.pi)
Rlon = Rlat/math.cos(self._lat0*math.pi/180.0)
self._aircraftinfos.track_angle_deg = random.uniform(0,360.0)
self._aircraftinfos.lat_deg = self._lat0 - random.uniform(-Rlat,Rlat)
self._aircraftinfos.lon_deg = self._lon0 + random.uniform(-Rlon,Rlon)
self._aircraftinfos.alt_msl_m = random.uniform(1.0,self._max_alt_m)
self._aircraftinfos.speed_mps = random.uniform(0.0,self._max_speed_mps)

View File

@ -0,0 +1,32 @@
""" implementation of a trajectory simulation where the simulated aircraft
is following a preplanned trajectory
mutex protection occurs when calling replace_message
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/>.
"""
import time
from AbstractTrajectorySimulatorBase import AbstractTrajectorySimulatorBase
class WaypointsTrajectorySimulator(AbstractTrajectorySimulatorBase):
def __init__(self,mutex,broadcast_thread,aircrafts_info,waypoints_file):
super().__init__(mutex,broadcast_thread)
def refresh_delay(self):
return 0.005
# TODO : implement waypoint simulation...
#def update_aircraftinfos(self):
# pass

BIN
adsb-out-circle.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

BIN
adsb-out-random.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

1028
pyhackrf.py Normal file

File diff suppressed because it is too large Load Diff

156
realtime-adsb-out.py Executable file
View File

@ -0,0 +1,156 @@
#!/usr/bin/env python3
""" This file hold the main function which read user inputs
initialize and launch the simulation
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/>.
"""
import sys, time, math
import threading
from AircraftInfos import AircraftInfos
from FixedTrajectorySimulator import FixedTrajectorySimulator
from PseudoCircleTrajectorySimulator import PseudoCircleTrajectorySimulator
from RandomTrajectorySimulator import RandomTrajectorySimulator
from WaypointsTrajectorySimulator import WaypointsTrajectorySimulator
from HackRfBroadcastThread import HackRfBroadcastThread
from getopt import getopt, GetoptError
def usage(msg=False):
if msg:print(msg)
print("Usage: %s [options]\n" % sys.argv[0])
print("-h | --help Display help message.")
print("--icao <opt> Callsign in hex, Default:0x508035")
print("--callsign <opt> Callsign (8 chars max), Default:DEADBEEF")
print("--squawk <opt> 4-digits 4096 code squawk, Default:7000")
print("--trajectorytype <opt> Type of simulated trajectory amongst :")
print(" fixed : steady aircraft")
print(" circle : pseudo circular flight")
print(" random : random positions inside circle area")
print(" waypoints : fly long flight path")
print(" Default:fixed")
print("--lat <opt> Latitude for the plane in decimal degrees, Default:50.44994")
print("--long <opt> Longitude for the place in decimal degrees. Default:30.5211")
print("--altitude <opt> Altitude in decimal feet, Default:1500.0")
print("--speed <opt> Airspeed in decimal kph, Default:300.0")
print("--vspeed <opt> Vertical speed en ft/min, positive up, Default:0")
print("--maxloadfactor Specify the max load factor for aircraft simulation. Default:1.45")
print("--trackangle <opt> Track angle in decimal degrees. Default:0")
print("--timesync <opt> 0/1, 0 indicates time not synchronous with UTC, Default:0")
print("--capability <opt> Capability, Default:5")
print("--typecode <opt> ADS-B message type, Default:11")
print("--sstatus <opt> Surveillance status, Default:0")
print("--nicsupplementb <opt> NIC supplement-B, Default:0")
print("--surface Aircraft located on ground, Default:False")
print("--waypoints <opt> Waypoints file for waypoints trajectory")
print("--posrate <opt> position frame broadcast period in µs, Default: 150000")
print("")
#print("see usage.md for additionnal information")
sys.exit(2)
def main():
# Default values
icao_aa = '0x508035'
callsign = 'DEADBEEF'
squawk = '7000'
alt_ft = 1500.0
lat_deg = 50.44994
lon_deg = 30.5211
speed_kph = 300.0
vspeed_ftpmin = 0.0
maxloadfactor = 1.45
track_angle_deg = 0.0
capability = 5
type_code = 11
surveillance_status = 0
timesync = 0
nicsup = 0
on_surface = False
trajectory_type = 'fixed'
waypoints_file = None
posrate = 150000
try:
(opts, args) = getopt(sys.argv[1:], 'h', \
['help','icao=','callsign=','squawk=','trajectorytype=','lat=','long=','altitude=','speed=','vspeed=','maxloadfactor=','trackangle=',
'timesync=','capability=','typecode=','sstatus=','nicsupplementb=','surface','posrate='
])
except GetoptError as err:
usage("%s\n" % err)
if len(opts) != 0:
for (opt, arg) in opts:
if opt in ('-h', '--help'):usage()
elif opt in ('--icao'):icao_aa = arg
elif opt in ('--callsign'):callsign = arg
elif opt in ('--squawk'):squawk = arg
elif opt in ('--trajectorytype'):trajectory_type = arg
elif opt in ('--lat'):lat_deg = float(arg)
elif opt in ('--long'):lon_deg = float(arg)
elif opt in ('--altitude'):alt_ft = float(arg)
elif opt in ('--speed'):speed_kph = float(arg)
elif opt in ('--vspeed'):vspeed_ftpmin = float(arg)
elif opt in ('--maxloadfactor'):maxloadfactor = float(arg)
elif opt in ('--trackangle'):track_angle_deg = float(arg)
elif opt in ('--timesync'):timesync = int(arg)
elif opt in ('--capability'):capability = int(arg)
elif opt in ('--typecode'):type_code = int(arg)
elif opt in ('--sstatus'):surveillance_status = int(arg)
elif opt in ('--nicsupplementb'):nicsup = int(arg)
elif opt in ('--surface'):on_surface = True
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)
# TODO : the mutex would better be implemented as an object attribute in broadcast thread
mutex = threading.Lock()
brodcast_thread = HackRfBroadcastThread(mutex,posrate) # posrate would usally be used with random mode to generate load of tracks
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)
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()
# 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()
print("reatime-adsb-out simulation is finished")
if __name__ == "__main__":
main()

BIN
test-setup.jpg Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 93 KiB