diff --git a/ADSBLowLevelEncoder.py b/ADSBLowLevelEncoder.py new file mode 100644 index 0000000..eebc745 --- /dev/null +++ b/ADSBLowLevelEncoder.py @@ -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) \ No newline at end of file diff --git a/AbstractTrajectorySimulatorBase.py b/AbstractTrajectorySimulatorBase.py new file mode 100644 index 0000000..3ba8863 --- /dev/null +++ b/AbstractTrajectorySimulatorBase.py @@ -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 . +""" + +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): + ... \ No newline at end of file diff --git a/AircraftInfos.py b/AircraftInfos.py new file mode 100644 index 0000000..5e4170a --- /dev/null +++ b/AircraftInfos.py @@ -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 . +""" + +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 \ No newline at end of file diff --git a/CustomDecorators.py b/CustomDecorators.py new file mode 100644 index 0000000..ebeb77b --- /dev/null +++ b/CustomDecorators.py @@ -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 diff --git a/FixedTrajectorySimulator.py b/FixedTrajectorySimulator.py new file mode 100644 index 0000000..b282996 --- /dev/null +++ b/FixedTrajectorySimulator.py @@ -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 . +""" + +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 \ No newline at end of file diff --git a/HackRfBroadcastThread.py b/HackRfBroadcastThread.py new file mode 100644 index 0000000..e022d7f --- /dev/null +++ b/HackRfBroadcastThread.py @@ -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 . +""" + +# +# 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 \ No newline at end of file diff --git a/ModeS.py b/ModeS.py new file mode 100644 index 0000000..2d1f66b --- /dev/null +++ b/ModeS.py @@ -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 . +############################################################### + +# 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) diff --git a/ModeSLocation.py b/ModeSLocation.py new file mode 100644 index 0000000..1a3ac00 --- /dev/null +++ b/ModeSLocation.py @@ -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 diff --git a/PseudoCircleTrajectorySimulator.py b/PseudoCircleTrajectorySimulator.py new file mode 100644 index 0000000..72013d9 --- /dev/null +++ b/PseudoCircleTrajectorySimulator.py @@ -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 . +""" + +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() \ No newline at end of file diff --git a/README.md b/README.md new file mode 100755 index 0000000..7b30cec --- /dev/null +++ b/README.md @@ -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) diff --git a/RandomTrajectorySimulator.py b/RandomTrajectorySimulator.py new file mode 100644 index 0000000..c3427d0 --- /dev/null +++ b/RandomTrajectorySimulator.py @@ -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 . +""" + +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) diff --git a/WaypointsTrajectorySimulator.py b/WaypointsTrajectorySimulator.py new file mode 100644 index 0000000..03d2caf --- /dev/null +++ b/WaypointsTrajectorySimulator.py @@ -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 . +""" + +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 \ No newline at end of file diff --git a/adsb-out-circle.png b/adsb-out-circle.png new file mode 100644 index 0000000..32f4ff7 Binary files /dev/null and b/adsb-out-circle.png differ diff --git a/adsb-out-random.png b/adsb-out-random.png new file mode 100644 index 0000000..8b85b6b Binary files /dev/null and b/adsb-out-random.png differ diff --git a/pyhackrf.py b/pyhackrf.py new file mode 100644 index 0000000..62f5c60 --- /dev/null +++ b/pyhackrf.py @@ -0,0 +1,1028 @@ +# +# Python wrapper for libhackrf +# +# Copyright 2019 Mathieu Peyrega +# +# This file is part of HackRF. +# +# 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 2, 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; see the file COPYING. If not, write to +# the Free Software Foundation, Inc., 51 Franklin Street, +# Boston, MA 02110-1301, USA. +# + +import logging +from ctypes import * +from enum import IntEnum + +logging.basicConfig() + + +# +# libhackrf enums +# +class LibHackRfReturnCode(IntEnum): + HACKRF_SUCCESS = 0 + HACKRF_TRUE = 1 + HACKRF_ERROR_INVALID_PARAM = -2 + HACKRF_ERROR_NOT_FOUND = -5 + HACKRF_ERROR_BUSY = -6 + HACKRF_ERROR_NO_MEM = -11 + HACKRF_ERROR_LIBUSB = -1000 + HACKRF_ERROR_THREAD = -1001 + HACKRF_ERROR_STREAMING_THREAD_ERR = -1002 + HACKRF_ERROR_STREAMING_STOPPED = -1003 + HACKRF_ERROR_STREAMING_EXIT_CALLED = -1004 + HACKRF_ERROR_USB_API_VERSION = -1005 + HACKRF_ERROR_NOT_LAST_DEVICE = -2000 + HACKRF_ERROR_OTHER = -9999 + + +class LibHackRfBoardIds(IntEnum): + BOARD_ID_JELLYBEAN = 0 + BOARD_ID_JAWBREAKER = 1 + BOARD_ID_HACKRF_ONE = 2 + BOARD_ID_RAD1O = 3 + BOARD_ID_INVALID = 0xFF + + +class LibHackRfUSBBoardIds(IntEnum): + USB_BOARD_ID_JAWBREAKER = 0x604B + USB_BOARD_ID_HACKRF_ONE = 0x6089 + USB_BOARD_ID_RAD1O = 0xCC15 + USB_BOARD_ID_INVALID = 0xFFFF + + +class LibHackRfPathFilter(IntEnum): + RF_PATH_FILTER_BYPASS = 0 + RF_PATH_FILTER_LOW_PASS = 1 + RF_PATH_FILTER_HIGH_PASS = 2 + + +class LibHackRfTransceiverMode(IntEnum): + TRANSCEIVER_MODE_OFF = 0 + TRANSCEIVER_MODE_RX = 1 + TRANSCEIVER_MODE_TX = 2 + TRANSCEIVER_MODE_SS = 3 + + +class LibHackRfHwMode(IntEnum): + HW_MODE_OFF = 0 + HW_MODE_ON = 1 + + +# +# C structs or datatypes needed to interface Python and C +# +hackrf_device_p = c_void_p + + +class hackrf_transfer(Structure): + _fields_ = [("device", hackrf_device_p), + ("buffer", POINTER(c_ubyte)), + ("buffer_length", c_int), + ("valid_length", c_int), + ("rx_ctx", c_void_p), + ("tx_ctx", c_void_p)] + + +class read_partid_serialno_t(Structure): + _fields_ = [("part_id", c_uint32 * 2), + ("serial_no", c_uint32 * 4)] + + +class hackrf_device_list_t(Structure): + _fields_ = [("serial_numbers", POINTER(c_char_p)), + ("usb_board_ids", POINTER(c_int)), + ("usb_device_index", POINTER(c_int)), + ("devicecount", c_int), + ("usb_devices", POINTER(c_void_p)), + ("usb_devicecount", c_int)] + + +hackrf_transfer_callback_t = CFUNCTYPE(c_int, POINTER(hackrf_transfer)) + + +# +# libhackrf Python wrapper class +# +class HackRF(object): + # Class attibutes + __libhackrf = None + __libhackrfpath = None + __libraryversion = None + __libraryrelease = None + __instances = list() + __openedInstances = dict() + __logger = logging.getLogger("pyHackRF") + __logger.setLevel(logging.CRITICAL) + + @classmethod + def setLogLevel(cls, level): + cls.__logger.setLevel(level) + + def __init__(self, libhackrf_path='libhackrf.so.0'): + if (not __class__.initialized()): + __class__.initialize(libhackrf_path) + else: + __class__.__logger.debug("Instanciating " + __class__.__name__ + " object number #%d", + len(__class__.__instances)) + + # Instances attributes + # Description, serial and internals + self.__pDevice = hackrf_device_p(None) + self.__boardId = None + self.__usbboardId = None + self.__usbIndex = None + self.__usbAPIVersion = None + self.__boardFwVersionString = None + self.__partId = None + self.__serialNo = None + self.__CPLDcrc = None + self.__txCallback = None + self.__rxCallback = None + # RF state and settings + self.__transceiverMode = LibHackRfTransceiverMode.TRANSCEIVER_MODE_OFF + self.__hwSyncMode = LibHackRfHwMode.HW_MODE_OFF + self.__clockOutMode = LibHackRfHwMode.HW_MODE_OFF + self.__amplificatorMode = LibHackRfHwMode.HW_MODE_OFF + self.__antennaPowerMode = LibHackRfHwMode.HW_MODE_OFF + + self.__crystalppm = 0. + + # trial to implement getters, but this would probably be better to + # have it done from the .c library rather than a DIY solution here + # relevant parts have been commented out + # self.__lnaGain = 0 + # self.__vgaGain = 0 + # self.__txvgaGain = 0 + # self.__basebandFilterBandwidth = 0 + # self.__frequency = 0 + # self.__loFrequency = 0 + # self.__rfFilterPath = LibHackRfPathFilter.RF_PATH_FILTER_BYPASS + + __class__.__instances.append(self) + + def __del__(self): + __class__.__instances.remove(self) + __class__.__logger.debug(__class__.__name__ + " __del__ being called") + if (len(__class__.__instances) == 0): + __class__.__logger.debug(__class__.__name__ + " __del__ being called on the last instance") + + @classmethod + def initialized(cls): + return cls.__libhackrf is not None + + @classmethod + def initialize(cls, libhackrf_path='libhackrf.so.0'): + result = LibHackRfReturnCode.HACKRF_ERROR_OTHER + if (not cls.initialized()): + cls.__libhackrfpath = libhackrf_path + cls.__libhackrf = CDLL(cls.__libhackrfpath) + + # + # begin of C to Python bindings + # + + # + # Library initialization / deinitialization + # + + # extern ADDAPI int ADDCALL hackrf_init(); + cls.__libhackrf.hackrf_init.restype = c_int + cls.__libhackrf.hackrf_init.argtypes = [] + + # extern ADDAPI int ADDCALL hackrf_exit(); + cls.__libhackrf.hackrf_exit.restype = c_int + cls.__libhackrf.hackrf_exit.argtypes = [] + + # extern ADDAPI const char* ADDCALL hackrf_library_version(); + cls.__libhackrf.hackrf_library_version.restype = c_char_p + cls.__libhackrf.hackrf_library_version.argtypes = [] + # extern ADDAPI const char* ADDCALL hackrf_library_release(); + cls.__libhackrf.hackrf_library_release.restype = c_char_p + cls.__libhackrf.hackrf_library_release.argtypes = [] + + # extern ADDAPI const char* ADDCALL hackrf_error_name(enum hackrf_error errcode); + cls.__libhackrf.hackrf_error_name.restype = c_char_p + cls.__libhackrf.hackrf_error_name.argtypes = [c_int] + + # extern ADDAPI int ADDCALL hackrf_open(hackrf_device** device); + # purposely not offered as a replacement logic is set-up + + # + # Not implemented yet + # + + # extern ADDAPI int ADDCALL hackrf_max2837_read(hackrf_device* device, uint8_t register_number, uint16_t* value); + # extern ADDAPI int ADDCALL hackrf_max2837_write(hackrf_device* device, uint8_t register_number, uint16_t value); + # extern ADDAPI int ADDCALL hackrf_si5351c_read(hackrf_device* device, uint16_t register_number, uint16_t* value); + # extern ADDAPI int ADDCALL hackrf_si5351c_write(hackrf_device* device, uint16_t register_number, uint16_t value); + # extern ADDAPI int ADDCALL hackrf_rffc5071_read(hackrf_device* device, uint8_t register_number, uint16_t* value); + # extern ADDAPI int ADDCALL hackrf_rffc5071_write(hackrf_device* device, uint8_t register_number, uint16_t value); + # extern ADDAPI int ADDCALL hackrf_spiflash_erase(hackrf_device* device); + # extern ADDAPI int ADDCALL hackrf_spiflash_write(hackrf_device* device, const uint32_t address, const uint16_t length, unsigned char* const data); + # extern ADDAPI int ADDCALL hackrf_spiflash_read(hackrf_device* device, const uint32_t address, const uint16_t length, unsigned char* data); + # extern ADDAPI int ADDCALL hackrf_spiflash_status(hackrf_device* device, uint8_t* data); + # extern ADDAPI int ADDCALL hackrf_spiflash_clear_status(hackrf_device* device); + # extern ADDAPI int ADDCALL hackrf_cpld_write(hackrf_device* device, unsigned char* const data, const unsigned int total_length); + # extern ADDAPI int ADDCALL hackrf_get_operacake_boards(hackrf_device* device, uint8_t* boards); + # extern ADDAPI int ADDCALL hackrf_set_operacake_ports(hackrf_device* device, uint8_t address, uint8_t port_a, uint8_t port_b); + # extern ADDAPI int ADDCALL hackrf_set_operacake_ranges(hackrf_device* device, uint8_t* ranges, uint8_t num_ranges); + # extern ADDAPI int ADDCALL hackrf_operacake_gpio_test(hackrf_device* device, uint8_t address, uint16_t* test_result); + + # + # General low level hardware management + # list, open, close + # + + # extern ADDAPI hackrf_device_list_t* ADDCALL hackrf_device_list(); + cls.__libhackrf.hackrf_device_list.restype = POINTER(hackrf_device_list_t) + cls.__libhackrf.hackrf_device_list.argtypes = [] + + # extern ADDAPI void ADDCALL hackrf_device_list_free(hackrf_device_list_t *list); + cls.__libhackrf.hackrf_device_list_free.restype = None + cls.__libhackrf.hackrf_device_list_free.argtypes = [POINTER(hackrf_device_list_t)] + + # extern ADDAPI int ADDCALL hackrf_open(hackrf_device** device); + cls.__libhackrf.hackrf_open.restype = c_int + cls.__libhackrf.hackrf_open.argtypes = [POINTER(hackrf_device_p)] + + # extern ADDAPI int ADDCALL hackrf_open_by_serial(const char* const desired_serial_number, hackrf_device** device); + cls.__libhackrf.hackrf_open_by_serial.restype = c_int + cls.__libhackrf.hackrf_open_by_serial.arg_types = [c_char_p, POINTER(hackrf_device_p)] + + # extern ADDAPI int ADDCALL hackrf_device_list_open(hackrf_device_list_t *list, int idx, hackrf_device** device); + cls.__libhackrf.hackrf_device_list_open.restype = c_int + cls.__libhackrf.hackrf_device_list_open.arg_types = [POINTER(hackrf_device_list_t), c_int, + POINTER(hackrf_device_p)] + + # extern ADDAPI int ADDCALL hackrf_close(hackrf_device* device); + cls.__libhackrf.hackrf_close.restype = c_int + cls.__libhackrf.hackrf_close.argtypes = [hackrf_device_p] + + # extern ADDAPI int ADDCALL hackrf_reset(hackrf_device* device); + cls.__libhackrf.hackrf_reset.restype = c_int + cls.__libhackrf.hackrf_reset.argtypes = [hackrf_device_p] + + # extern ADDAPI int ADDCALL hackrf_board_id_read(hackrf_device* device, uint8_t* value); + cls.__libhackrf.hackrf_board_id_read.restype = c_int + cls.__libhackrf.hackrf_board_id_read.argtypes = [hackrf_device_p, POINTER(c_uint8)] + + # extern ADDAPI int ADDCALL hackrf_version_string_read(hackrf_device* device, char* version, uint8_t length); + cls.__libhackrf.hackrf_version_string_read.restype = c_int + cls.__libhackrf.hackrf_version_string_read.argtypes = [hackrf_device_p, POINTER(c_char), c_uint8] + + # extern ADDAPI int ADDCALL hackrf_usb_api_version_read(hackrf_device* device, uint16_t* version); + cls.__libhackrf.hackrf_usb_api_version_read.restype = c_int + cls.__libhackrf.hackrf_usb_api_version_read.argtypes = [hackrf_device_p, POINTER(c_uint16)] + + # extern ADDAPI int ADDCALL hackrf_board_partid_serialno_read(hackrf_device* device, read_partid_serialno_t* read_partid_serialno); + cls.__libhackrf.hackrf_board_partid_serialno_read.restype = c_int + cls.__libhackrf.hackrf_board_partid_serialno_read.argtypes = [hackrf_device_p, + POINTER(read_partid_serialno_t)] + + # extern ADDAPI int ADDCALL hackrf_cpld_checksum(hackrf_device* device, uint32_t* crc); + # this is now disabled by default in libhackrf (see hackrf.h line 323) + #cls.__libhackrf.hackrf_cpld_checksum.restype = c_int + #cls.__libhackrf.hackrf_cpld_checksum.argtypes = [hackrf_device_p, POINTER(c_uint32)] + + # extern ADDAPI const char* ADDCALL hackrf_board_id_name(enum hackrf_board_id board_id); + cls.__libhackrf.hackrf_board_id_name.restype = c_char_p + cls.__libhackrf.hackrf_board_id_name.argtypes = [c_int] + # extern ADDAPI const char* ADDCALL hackrf_usb_board_id_name(enum hackrf_usb_board_id usb_board_id); + cls.__libhackrf.hackrf_usb_board_id_name.restype = c_char_p + cls.__libhackrf.hackrf_usb_board_id_name.argtypes = [c_int] + + # extern ADDAPI const char* ADDCALL hackrf_filter_path_name(const enum rf_path_filter path); + cls.__libhackrf.hackrf_filter_path_name.restype = c_char_p + cls.__libhackrf.hackrf_filter_path_name.argtypes = [c_int] + + # extern ADDAPI int ADDCALL hackrf_set_hw_sync_mode(hackrf_device* device, const uint8_t value); + cls.__libhackrf.hackrf_set_hw_sync_mode.restype = c_int + cls.__libhackrf.hackrf_set_hw_sync_mode.argtypes = [hackrf_device_p, c_uint8] + + # extern ADDAPI int ADDCALL hackrf_set_clkout_enable(hackrf_device* device, const uint8_t value); + cls.__libhackrf.hackrf_set_clkout_enable.restype = c_int + cls.__libhackrf.hackrf_set_clkout_enable.argtypes = [hackrf_device_p, c_uint8] + + # + # RF settings + # + # extern ADDAPI int ADDCALL hackrf_set_baseband_filter_bandwidth(hackrf_device* device, const uint32_t bandwidth_hz); + cls.__libhackrf.hackrf_set_baseband_filter_bandwidth.restype = c_int + cls.__libhackrf.hackrf_set_baseband_filter_bandwidth.argtypes = [hackrf_device_p, c_uint32] + + # extern ADDAPI int ADDCALL hackrf_set_freq(hackrf_device* device, const uint64_t freq_hz); + cls.__libhackrf.hackrf_set_freq.restype = c_int + cls.__libhackrf.hackrf_set_freq.argtypes = [hackrf_device_p, c_uint64] + + # extern ADDAPI int ADDCALL hackrf_set_freq_explicit(hackrf_device* device, const uint64_t if_freq_hz, const uint64_t lo_freq_hz, const enum rf_path_filter path); + cls.__libhackrf.hackrf_set_freq_explicit.restype = c_int + cls.__libhackrf.hackrf_set_freq_explicit.argtypes = [hackrf_device_p, c_uint64, c_uint64, c_uint32] + + # extern ADDAPI int ADDCALL hackrf_set_sample_rate_manual(hackrf_device* device, const uint32_t freq_hz, const uint32_t divider); + cls.__libhackrf.hackrf_set_sample_rate_manual.restype = c_int + cls.__libhackrf.hackrf_set_sample_rate_manual.argtypes = [hackrf_device_p, c_uint32, c_uint32] + + # extern ADDAPI int ADDCALL hackrf_set_sample_rate(hackrf_device* device, const double freq_hz); + cls.__libhackrf.hackrf_set_sample_rate.restype = c_int + cls.__libhackrf.hackrf_set_sample_rate.argtypes = [hackrf_device_p, c_double] + + # extern ADDAPI int ADDCALL hackrf_set_lna_gain(hackrf_device* device, uint32_t value); + cls.__libhackrf.hackrf_set_lna_gain.restype = c_int + cls.__libhackrf.hackrf_set_lna_gain.argtypes = [hackrf_device_p, c_uint32] + + # extern ADDAPI int ADDCALL hackrf_set_vga_gain(hackrf_device* device, uint32_t value); + cls.__libhackrf.hackrf_set_vga_gain.restype = c_int + cls.__libhackrf.hackrf_set_vga_gain.argtypes = [hackrf_device_p, c_uint32] + + # extern ADDAPI int ADDCALL hackrf_set_txvga_gain(hackrf_device* device, uint32_t value); + cls.__libhackrf.hackrf_set_txvga_gain.restype = c_int + cls.__libhackrf.hackrf_set_txvga_gain.argtypes = [hackrf_device_p, c_uint32] + + # extern ADDAPI int ADDCALL hackrf_set_amp_enable(hackrf_device* device, const uint8_t value); + cls.__libhackrf.hackrf_set_amp_enable.restype = c_int + cls.__libhackrf.hackrf_set_amp_enable.argtypes = [hackrf_device_p, c_uint8] + + # extern ADDAPI int ADDCALL hackrf_set_antenna_enable(hackrf_device* device, const uint8_t value); + cls.__libhackrf.hackrf_set_antenna_enable.restype = c_int + cls.__libhackrf.hackrf_set_antenna_enable.argtypes = [hackrf_device_p, c_uint8] + + # extern ADDAPI uint32_t ADDCALL hackrf_compute_baseband_filter_bw_round_down_lt(const uint32_t bandwidth_hz); + cls.__libhackrf.hackrf_compute_baseband_filter_bw_round_down_lt.restype = c_int + cls.__libhackrf.hackrf_compute_baseband_filter_bw_round_down_lt.argtypes = [c_uint32] + + # extern ADDAPI uint32_t ADDCALL hackrf_compute_baseband_filter_bw(const uint32_t bandwidth_hz); + cls.__libhackrf.hackrf_compute_baseband_filter_bw.restype = c_int + cls.__libhackrf.hackrf_compute_baseband_filter_bw.argtypes = [c_uint32] + + # + # Transfers management + # + + # extern ADDAPI int ADDCALL hackrf_is_streaming(hackrf_device* device); + cls.__libhackrf.hackrf_is_streaming.restype = c_int + cls.__libhackrf.hackrf_is_streaming.argtypes = [hackrf_device_p] + + # extern ADDAPI int ADDCALL hackrf_start_rx(hackrf_device* device, hackrf_sample_block_cb_fn callback, void* rx_ctx); + cls.__libhackrf.hackrf_start_rx.restype = c_int + cls.__libhackrf.hackrf_start_rx.argtypes = [hackrf_device_p, hackrf_transfer_callback_t, c_void_p] + + # extern ADDAPI int ADDCALL hackrf_start_tx(hackrf_device* device, hackrf_sample_block_cb_fn callback, void* tx_ctx); + cls.__libhackrf.hackrf_start_tx.restype = c_int + cls.__libhackrf.hackrf_start_tx.argtypes = [hackrf_device_p, hackrf_transfer_callback_t, c_void_p] + + # extern ADDAPI int ADDCALL hackrf_stop_rx(hackrf_device* device); + cls.__libhackrf.hackrf_stop_rx.restype = c_int + cls.__libhackrf.hackrf_stop_rx.argtypes = [hackrf_device_p] + + # extern ADDAPI int ADDCALL hackrf_stop_tx(hackrf_device* device); + cls.__libhackrf.hackrf_stop_tx.restype = c_int + cls.__libhackrf.hackrf_stop_tx.argtypes = [hackrf_device_p] + + # extern ADDAPI int ADDCALL hackrf_init_sweep(hackrf_device* device, const uint16_t* frequency_list, const int num_ranges, const uint32_t num_bytes, const uint32_t step_width, const uint32_t offset, const enum sweep_style style); + cls.__libhackrf.hackrf_init_sweep.restype = c_int + cls.__libhackrf.hackrf_init_sweep.argtypes = [hackrf_device_p, POINTER(c_uint16), c_int, c_uint32, c_uint32, + c_uint32, c_uint32] + + # + # end of C to Python bindings + # + + result = cls.__libhackrf.hackrf_init() + + if (result != LibHackRfReturnCode.HACKRF_SUCCESS): + cls.__logger.error( + cls.__name__ + " class initialization failed, error=(%d," + __class__.getHackRfErrorCodeName( + result) + ")", result) + else: + cls.__libraryversion = cls.__libhackrf.hackrf_library_version().decode("UTF-8") + cls.__logger.debug(cls.__name__ + " library version : " + cls.__libraryversion) + cls.__libraryrelease = cls.__libhackrf.hackrf_library_release().decode("UTF-8") + cls.__logger.debug(cls.__name__ + " library release : " + cls.__libraryrelease) + cls.__logger.debug(cls.__name__ + " class initialization successfull") + + else: + __class__.__logger.debug(cls.__name__ + " is already initialized") + return result + + @classmethod + def getInstanceByDeviceHandle(cls, pDevice): + return cls.__openedInstances.get(pDevice, None) + + @classmethod + def getDeviceListPointer(cls): + if (not __class__.initialized()): + __class__.initialize() + + pHackRfDeviceList = cls.__libhackrf.hackrf_device_list() + return pHackRfDeviceList + + @classmethod + def freeDeviceList(cls, pList): + if (not cls.initialized()): + cls.initialize() + + pHackRfDeviceList = cls.__libhackrf.hackrf_device_list_free(pList) + + @classmethod + def getHackRfErrorCodeName(cls, ec): + if (not cls.initialized()): + cls.initialize() + + return cls.__libhackrf.hackrf_error_name(ec).decode("UTF-8") + + @classmethod + def getBoardNameById(cls, bid): + if (not cls.initialized()): + cls.initialize() + + return cls.__libhackrf.hackrf_board_id_name(bid).decode("UTF-8") + + @classmethod + def getUsbBoardNameById(cls, usbbid): + if (not cls.initialized()): + cls.initialize() + + return cls.__libhackrf.hackrf_usb_board_id_name(usbbid).decode("UTF-8") + + @classmethod + def getHackRFFilterPathNameById(cls, rfpid): + if (not cls.initialized()): + cls.initialize() + + return cls.__libhackrf.hackrf_filter_path_name(rfpid).decode("UTF-8") + + @classmethod + def deinitialize(cls): + result = LibHackRfReturnCode.HACKRF_ERROR_OTHER + if (cls.initialized()): + for hackrf in cls.__instances: + hackrf.stop() + + result = cls.__libhackrf.hackrf_exit() + if (result == LibHackRfReturnCode.HACKRF_SUCCESS): + cls.__libhackrf = None + else: + cls.__logger.error( + cls.__name__ + " class deinitialization failed, error=(%d," + __class__.getHackRfErrorCodeName( + result) + ")", result) + + return result + + @classmethod + def getLibraryVersion(cls): + if (not cls.initialized()): + cls.initialize() + + return cls.__libraryversion + + @classmethod + def getLibraryRelease(cls): + if (not cls.initialized()): + cls.initialize() + + return cls.__libraryrelease + + @classmethod + def computeBaseBandFilterBw(cls, bandwidth): + if (not cls.initialized()): + cls.initialize() + + return cls.__libhackrf.hackrf_compute_baseband_filter_bw(bandwidth) + + @classmethod + def computeBaseBandFilterBwRoundDownLt(cls, bandwidth): + if (not cls.initialized()): + cls.initialize() + + return cls.__libhackrf.hackrf_compute_baseband_filter_bw_round_down_lt(bandwidth) + + def opened(self): + return self.__pDevice.value is not None + + def closed(self): + return self.__pDevice.value is None + + def open(self, openarg=-1): + result = LibHackRfReturnCode.HACKRF_ERROR_OTHER + if not self.opened(): + if (isinstance(openarg, int)): + result = self.__openByIndex(openarg) + elif (isinstance(openarg, str)): + result = self.__openBySerial(openarg.lower()) + else: + __class__.__logger.debug("Trying to open an already opened " + __class__.__name__) + if (result == LibHackRfReturnCode.HACKRF_SUCCESS): + __class__.__openedInstances[self.__pDevice.value] = self + return result + + def close(self): + __class__.__logger.debug("Trying to close a " + __class__.__name__) + result = LibHackRfReturnCode.HACKRF_ERROR_OTHER + if self.opened(): + result = __class__.__libhackrf.hackrf_close(self.__pDevice) + if (result != LibHackRfReturnCode.HACKRF_SUCCESS): + __class__.__logger.error( + "Error (%d," + __class__.getHackRfErrorCodeName(result) + ") while closing a " + __class__.__name__, + result) + else: + __class__.__logger.info("Success closing " + __class__.__name__) + __class__.__openedInstances.pop(self.__pDevice.value, None) + self.__pDevice.value = None + self.__boardId = None + self.__usbboardId = None + self.__usbIndex = None + self.__usbAPIVersion = None + self.__boardFwVersionString = None + self.__partId = None + self.__serialNo = None + self.__CPLDcrc = None + self.__txCallback = None + self.__rxCallback = None + self.__transceiverMode = LibHackRfTransceiverMode.TRANSCEIVER_MODE_OFF + self.__hwSyncMode = LibHackRfHwMode.HW_MODE_OFF + self.__clockOutMode = LibHackRfHwMode.HW_MODE_OFF + self.__amplificatorMode = LibHackRfHwMode.HW_MODE_OFF + self.__antennaPowerMode = LibHackRfHwMode.HW_MODE_OFF + self.__crystalppm = 0 + # self.__lnaGain = 0 + # self.__vgaGain = 0 + # self.__txvgaGain = 0 + # self.__basebandFilterBandwidth = 0 + # self.__frequency = 0 + # self.__loFrequency = 0 + # self.__rfFilterPath = LibHackRfPathFilter.RF_PATH_FILTER_BYPASS + + else: + __class__.__logger.debug("Trying to close a non-opened " + __class__.__name__) + return result + + def reset(self): + __class__.__logger.debug("Trying to reset a " + __class__.__name__) + result = LibHackRfReturnCode.HACKRF_ERROR_OTHER + if self.opened(): + result = __class__.__libhackrf.hackrf_reset(self.__pDevice) + else: + __class__.__logger.debug("Trying to reset a non-opened " + __class__.__name__) + return result + + def getBoardSerialNumberString(self, words_separator=''): + if not self.opened(): + __class__.__logger.error( + __class__.__name__ + " getBoardSerialNumberString() has been called on a closed instance") + raise Exception(__class__.__name__ + " getBoardSerialNumberString() has been called on a closed instance") + else: + return ( + "{:08x}" + words_separator + "{:08x}" + words_separator + "{:08x}" + words_separator + "{:08x}").format( + self.__serialNo[0], self.__serialNo[1], self.__serialNo[2], self.__serialNo[3]) + + def getBoardSerialNumber(self): + if not self.opened(): + __class__.__logger.error( + __class__.__name__ + " getBoardSerialNumber() has been called on a closed instance") + raise Exception(__class__.__name__ + " getBoardSerialNumber() has been called on a closed instance") + else: + return self.__serialNo + + def __readBoardSerialNumber(self): + # Board SerialNo and PartID + serinfo = read_partid_serialno_t((-1, -1), (-1, -1, -1, -1)) + result = __class__.__libhackrf.hackrf_board_partid_serialno_read(self.__pDevice, byref(serinfo)) + if (result != LibHackRfReturnCode.HACKRF_SUCCESS): + __class__.__logger.error( + "Error (%d," + __class__.getHackRfErrorCodeName(result) + ") on hackrf_board_partid_serialno_read", + result) + else: + self.__partId = (serinfo.part_id[0], serinfo.part_id[1]) + __class__.__logger.debug( + __class__.__name__ + " opened board part id : " + "{:08x}:{:08x}".format(self.__partId[0], + self.__partId[1])) + self.__serialNo = (serinfo.serial_no[0], serinfo.serial_no[1], serinfo.serial_no[2], serinfo.serial_no[3]) + __class__.__logger.debug( + __class__.__name__ + " opened board serial number : " + self.getBoardSerialNumberString(':')) + + def __openByIndex(self, deviceindex): + result = LibHackRfReturnCode.HACKRF_ERROR_OTHER + + pHDL = __class__.getDeviceListPointer() + + if (deviceindex == -1): + __class__.__logger.debug("Try to open first available HackRF") + __class__.__logger.debug("%d devices detected", pHDL.contents.devicecount) + for index in range(0, pHDL.contents.devicecount): + __class__.__logger.debug("trying to open device index %d", index) + result = self.open(index) + if (result == LibHackRfReturnCode.HACKRF_SUCCESS): + break + else: + __class__.__logger.debug("tested hackrf not available") + + else: + __class__.__logger.debug("Trying to open HackRF with index=%d", deviceindex) + + result = __class__.__libhackrf.hackrf_device_list_open(pHDL, deviceindex, byref(self.__pDevice)) + if (result != LibHackRfReturnCode.HACKRF_SUCCESS): + __class__.__logger.error("Error (%d," + __class__.getHackRfErrorCodeName( + result) + ") while opening " + __class__.__name__ + " with index=%d", result, deviceindex) + else: + self.__readBoardSerialNumber() + self.__usbboardId = pHDL.contents.usb_board_ids[deviceindex] + self.__usbIndex = pHDL.contents.usb_device_index[deviceindex] + self.__readBoardInfos() + __class__.__logger.info("Success opening " + __class__.__name__) + + __class__.freeDeviceList(pHDL) + return result + + def __openBySerial(self, deviceserial): + __class__.__logger.debug("Trying to open a HackRF by serial number: " + deviceserial) + result = LibHackRfReturnCode.HACKRF_ERROR_OTHER + result = __class__.__libhackrf.hackrf_open_by_serial(c_char_p(deviceserial.encode("UTF-8")), + byref(self.__pDevice)) + if (result != LibHackRfReturnCode.HACKRF_SUCCESS): + __class__.__logger.error("Error (%d," + __class__.getHackRfErrorCodeName( + result) + ") while opening " + __class__.__name__ + " with serial number=" + deviceserial, result) + else: + self.__readBoardSerialNumber() + pHDL = __class__.getDeviceListPointer() + for deviceindex in range(0, pHDL.contents.devicecount): + if pHDL.contents.serial_numbers[deviceindex].decode("UTF-8") == self.getBoardSerialNumberString(): + self.__usbboardId = pHDL.contents.usb_board_ids[deviceindex] + self.__usbIndex = pHDL.contents.usb_device_index[deviceindex] + break; + __class__.freeDeviceList(pHDL) + self.__readBoardInfos() + __class__.__logger.info("Success opening " + __class__.__name__) + + return result + + def __readBoardInfos(self): + if not self.opened(): + __class__.__logger.error(__class__.__name__ + " __readBoardInfos() has been called on a closed instance") + else: + # Board Id + bId = c_uint8(0) + result = __class__.__libhackrf.hackrf_board_id_read(self.__pDevice, byref(bId)) + if (result != LibHackRfReturnCode.HACKRF_SUCCESS): + __class__.__logger.error( + "Error (%d," + __class__.getHackRfErrorCodeName(result) + ") on hackrf_board_id_read", result) + else: + self.__boardId = LibHackRfBoardIds(bId.value) + __class__.__logger.debug( + __class__.__name__ + " opened board id : %d, " + self.__boardId.name + ", " + __class__.getBoardNameById( + self.__boardId), self.__boardId.value) + __class__.__logger.debug(__class__.__name__ + " opened usbboard id : " + "{:04x}, ".format( + self.__usbboardId) + __class__.getUsbBoardNameById(self.__usbboardId)) + + # Board Firmware Version + bfwversion_size = 128 + bfwversion = (c_char * bfwversion_size)() + result = __class__.__libhackrf.hackrf_version_string_read(self.__pDevice, bfwversion, bfwversion_size) + if (result != LibHackRfReturnCode.HACKRF_SUCCESS): + __class__.__logger.error( + "Error (%d," + __class__.getHackRfErrorCodeName(result) + ") on hackrf_version_string_read", result) + else: + self.__boardFwVersionString = bfwversion.value.decode("UTF-8") + __class__.__logger.debug( + __class__.__name__ + " opened board firmware version : " + self.__boardFwVersionString) + + # Board USB API version + bUSB_API_ver = c_uint16(0) + result = __class__.__libhackrf.hackrf_usb_api_version_read(self.__pDevice, byref(bUSB_API_ver)) + if (result != LibHackRfReturnCode.HACKRF_SUCCESS): + __class__.__logger.error( + "Error (%d," + __class__.getHackRfErrorCodeName(result) + ") on hackrf_usb_api_version_read", + result) + else: + self.__usbAPIVersion = bUSB_API_ver.value + __class__.__logger.debug( + __class__.__name__ + " opened board USB API version : " + "{:02x}:{:02x}".format( + self.__usbAPIVersion >> 8, self.__usbAPIVersion & 0xFF)) + + # Board CLPD checksum + #cpld_checsum = c_uint32(-1) + #result = __class__.__libhackrf.hackrf_cpld_checksum(self.__pDevice, byref(cpld_checsum)) + #if (result != LibHackRfReturnCode.HACKRF_SUCCESS): + # __class__.__logger.error( + # "Error (%d," + __class__.getHackRfErrorCodeName(result) + ") on hackrf_cpld_checksum", result) + #else: + # self.__CPLDcrc = cpld_checsum.value + # __class__.__logger.debug( + # __class__.__name__ + " opened board CPLD checksum : " + "{:08x}".format(self.__CPLDcrc)) + + def printBoardInfos(self): + if not self.opened(): + print(__class__.__name__ + " is closed and informations cannot be displayed") + else: + print(__class__.__name__ + " board id : " + "{:d}".format( + self.__boardId.value) + ", " + self.__boardId.name) + print(__class__.__name__ + " board name : " + __class__.getBoardNameById(self.__boardId)) + print(__class__.__name__ + " board USB id : " + "0x{:04x}".format( + self.__usbboardId) + ", " + __class__.getUsbBoardNameById(self.__usbboardId)) + print(__class__.__name__ + " board USB index : " + "0x{:04x}".format(self.__usbIndex)) + print(__class__.__name__ + " board USB API : " + "{:02x}:{:02x}".format(self.__usbAPIVersion >> 8, + self.__usbAPIVersion & 0xFF)) + print(__class__.__name__ + " board firmware : " + self.__boardFwVersionString) + print(__class__.__name__ + " board part id : " + "{:08x}:{:08x}".format(self.__partId[0], + self.__partId[1])) + print(__class__.__name__ + " board part id : " + self.getBoardSerialNumberString(':')) + #print(__class__.__name__ + " board CPLD checksum : " + "0x{:08x}".format(self.__CPLDcrc)) + + def stop(self): + # TODO : implement transfer stopping logics ? + self.close() + + def setHwSyncMode(self, mode): + __class__.__logger.debug(__class__.__name__ + " Trying to set HwSyncMode") + result = LibHackRfReturnCode.HACKRF_ERROR_OTHER + if self.opened(): + result = __class__.__libhackrf.hackrf_set_hw_sync_mode(self.__pDevice, c_uint8(LibHackRfHwMode(mode).value)) + if (result == LibHackRfReturnCode.HACKRF_SUCCESS): + self.__hwSyncMode = mode + else: + __class__.__logger.debug("Trying to set HwSyncMode for non-opened " + __class__.__name__) + return result + + def getHwSyncMode(self): + return self.__hwSyncMode + + def setClkOutMode(self, mode): + __class__.__logger.debug(__class__.__name__ + " Trying to set ClkOutMode") + result = LibHackRfReturnCode.HACKRF_ERROR_OTHER + if self.opened(): + result = __class__.__libhackrf.hackrf_set_clkout_enable(self.__pDevice, + c_uint8(LibHackRfHwMode(mode).value)) + if (result == LibHackRfReturnCode.HACKRF_SUCCESS): + self.__clockOutMode = mode + else: + __class__.__logger.debug("Trying to set ClkOutMode for non-opened " + __class__.__name__) + return result + + def getClkOutMode(self): + return self.__clockOutMode + + def setAmplifierMode(self, mode): + __class__.__logger.debug(__class__.__name__ + " Trying to set AmplifierMode") + result = LibHackRfReturnCode.HACKRF_ERROR_OTHER + if self.opened(): + result = __class__.__libhackrf.hackrf_set_amp_enable(self.__pDevice, c_uint8(LibHackRfHwMode(mode).value)) + if (result == LibHackRfReturnCode.HACKRF_SUCCESS): + self.__amplificatorMode = mode + else: + __class__.__logger.debug("Trying to set AmplifierMode for non-opened " + __class__.__name__) + return result + + def getAmplifierMode(self): + return self.__amplificatorMode + + def setAntennaPowerMode(self, mode): + __class__.__logger.debug(__class__.__name__ + " Trying to set AntennaPowerMode") + result = LibHackRfReturnCode.HACKRF_ERROR_OTHER + if self.opened(): + result = __class__.__libhackrf.hackrf_set_antenna_enable(self.__pDevice, + c_uint8(LibHackRfHwMode(mode).value)) + if (result == LibHackRfReturnCode.HACKRF_SUCCESS): + self.__antennaPowerMode = mode + else: + __class__.__logger.debug("Trying to set AntennaPowerMode for non-opened " + __class__.__name__) + return result + + def getAntennaPowerMode(self): + return self.__antennaPowerMode + + def isStreaming(self): + __class__.__logger.debug(__class__.__name__ + " Trying to call isStreaming") + if self.opened() and self.getTransceiverMode() != LibHackRfTransceiverMode.TRANSCEIVER_MODE_OFF: + return __class__.__libhackrf.hackrf_is_streaming(self.__pDevice) == LibHackRfReturnCode.HACKRF_TRUE + else: + print("isStreaming corner case") + __class__.__logger.debug( + "Trying to call isStreaming for non-opened or non transmitting " + __class__.__name__) + return False + + def stopRX(self): + __class__.__logger.debug(__class__.__name__ + " Trying to stop RX") + result = LibHackRfReturnCode.HACKRF_ERROR_OTHER + if self.opened() and self.getTransceiverMode() != LibHackRfTransceiverMode.TRANSCEIVER_MODE_OFF: + result = __class__.__libhackrf.hackrf_stop_rx(self.__pDevice) + if (result == LibHackRfReturnCode.HACKRF_SUCCESS): + __class__.__logger.info("Success stopping RX") + self.__transceiverMode = LibHackRfTransceiverMode.TRANSCEIVER_MODE_OFF + else: + __class__.__logger.error( + "Error (%d," + __class__.getHackRfErrorCodeName(result) + ") while stopping RX ", result) + else: + __class__.__logger.debug("Trying to stop RX for non-opened or non transmitting " + __class__.__name__) + return result + + def stopTX(self): + __class__.__logger.debug(__class__.__name__ + " Trying to stop TX") + result = LibHackRfReturnCode.HACKRF_ERROR_OTHER + if self.opened() and self.getTransceiverMode() != LibHackRfTransceiverMode.TRANSCEIVER_MODE_OFF: + result = __class__.__libhackrf.hackrf_stop_tx(self.__pDevice) + if (result == LibHackRfReturnCode.HACKRF_SUCCESS): + __class__.__logger.info("Success stopping TX") + self.__transceiverMode = LibHackRfTransceiverMode.TRANSCEIVER_MODE_OFF + else: + __class__.__logger.error( + "Error (%d," + __class__.getHackRfErrorCodeName(result) + ") while stopping TX ", result) + else: + __class__.__logger.debug("Trying to stop TX for non-opened or non transmitting " + __class__.__name__) + return result + + def startRX(self, callback, rx_context): + __class__.__logger.debug(__class__.__name__ + " Trying to start RX") + result = LibHackRfReturnCode.HACKRF_ERROR_OTHER + if self.opened() and self.getTransceiverMode() == LibHackRfTransceiverMode.TRANSCEIVER_MODE_OFF: + self.__rxCallback = hackrf_transfer_callback_t(callback) + if rx_context is None: + result = __class__.__libhackrf.hackrf_start_rx(self.__pDevice, self.__rxCallback, + None) + else: + result = __class__.__libhackrf.hackrf_start_rx(self.__pDevice, self.__rxCallback, + byref(rx_context)) + if (result == LibHackRfReturnCode.HACKRF_SUCCESS): + __class__.__logger.info("Success starting RX") + self.__transceiverMode = LibHackRfTransceiverMode.TRANSCEIVER_MODE_RX + else: + __class__.__logger.error( + "Error (%d," + __class__.getHackRfErrorCodeName(result) + ") while starting RX ", result) + else: + __class__.__logger.debug("Trying to start RX for non-opened or in transmission " + __class__.__name__) + return result + + def startTX(self, callback, tx_context): + __class__.__logger.debug(__class__.__name__ + " Trying to start TX") + result = LibHackRfReturnCode.HACKRF_ERROR_OTHER + if self.opened() and self.getTransceiverMode() == LibHackRfTransceiverMode.TRANSCEIVER_MODE_OFF: + self.__txCallback = hackrf_transfer_callback_t(callback) + if tx_context is None: + result = __class__.__libhackrf.hackrf_start_tx(self.__pDevice, self.__txCallback, + None) + else: + result = __class__.__libhackrf.hackrf_start_tx(self.__pDevice, self.__txCallback, + byref(tx_context)) + if (result == LibHackRfReturnCode.HACKRF_SUCCESS): + __class__.__logger.info("Success starting TX") + self.__transceiverMode = LibHackRfTransceiverMode.TRANSCEIVER_MODE_TX + else: + __class__.__logger.error( + "Error (%d," + __class__.getHackRfErrorCodeName(result) + ") while starting TX ", result) + else: + __class__.__logger.debug("Trying to start TX for non-opened or in transmission " + __class__.__name__) + return result + + def getTransceiverMode(self): + return self.__transceiverMode + + def setLNAGain(self, gain): + __class__.__logger.debug(__class__.__name__ + " Trying to set LNA gain") + result = LibHackRfReturnCode.HACKRF_ERROR_OTHER + if self.opened(): + result = __class__.__libhackrf.hackrf_set_lna_gain(self.__pDevice, gain) + # if (result == LibHackRfReturnCode.HACKRF_SUCCESS): + # self.__lnaGain = gain + else: + __class__.__logger.debug("Trying to set LNA gain for non-opened " + __class__.__name__) + return result + + # def getLNAGain(self): + # return self.__lnaGain + + def setVGAGain(self, gain): + __class__.__logger.debug(__class__.__name__ + " Trying to set VGA gain") + result = LibHackRfReturnCode.HACKRF_ERROR_OTHER + if self.opened(): + result = __class__.__libhackrf.hackrf_set_vga_gain(self.__pDevice, gain) + # if (result == LibHackRfReturnCode.HACKRF_SUCCESS): + # self.__vgaGain = gain + else: + __class__.__logger.debug("Trying to set VGA gain for non-opened " + __class__.__name__) + return result + + # def getVGAGain(self): + # return self.__vgaGain + + def setTXVGAGain(self, gain): + __class__.__logger.debug(__class__.__name__ + " Trying to set TX VGA gain") + result = LibHackRfReturnCode.HACKRF_ERROR_OTHER + if self.opened(): + result = __class__.__libhackrf.hackrf_set_txvga_gain(self.__pDevice, gain) + # if (result == LibHackRfReturnCode.HACKRF_SUCCESS): + # self.__txvgaGain = gain + else: + __class__.__logger.debug("Trying to set TX VGA gain for non-opened " + __class__.__name__) + return result + + # def getTXVGAGain(self): + # return self.__txvgaGain + + def setBasebandFilterBandwidth(self, bandwidth): + __class__.__logger.debug(__class__.__name__ + " Trying to set baseband filter bandwidth") + result = LibHackRfReturnCode.HACKRF_ERROR_OTHER + if self.opened(): + result = __class__.__libhackrf.hackrf_set_baseband_filter_bandwidth(self.__pDevice, bandwidth) + # if (result == LibHackRfReturnCode.HACKRF_SUCCESS): + # self.__basebandFilterBandwidth = bandwidth + else: + __class__.__logger.debug("Trying to set baseband filter bandwidth " + __class__.__name__) + return result + + # def getBasebandFilterBandwidth(self): + # return self.__basebandFilterBandwidth + + def setFrequency(self, frequency): + __class__.__logger.debug(__class__.__name__ + " Trying to set frequency") + result = LibHackRfReturnCode.HACKRF_ERROR_OTHER + if self.opened(): + result = __class__.__libhackrf.hackrf_set_freq(self.__pDevice, self.__correctFrequency(frequency)) + # if (result == LibHackRfReturnCode.HACKRF_SUCCESS): + # self.__frequency = frequency + else: + __class__.__logger.debug("Trying to set frequency " + __class__.__name__) + return result + + def __correctFrequency(self, frequency): + return int(frequency * (1.0 - (self.__crystalppm / 1000000.0))) + + def __correctSampleRate(self, samplerate): + # + # Not sure why the +0.5 is there. I copied it from hackrf_transfer + # equivalent source code but this is probably not necessary, especially + # because there is already a +0.5 done in hackrf.c hackrf_set_sample_rate + # + return int(samplerate * (1.0 - (self.__crystalppm / 1000000.0)) + 0.5) + + # def getFrequency(self): + # return self.__frequency + + def setFrequencyExplicit(self, if_frequency, lo_frequency, rf_path): + __class__.__logger.debug(__class__.__name__ + " Trying to set frequency with details") + result = LibHackRfReturnCode.HACKRF_ERROR_OTHER + if self.opened(): + result = __class__.__libhackrf.hackrf_set_freq_explicit(self.__pDevice, + self.__correctFrequency(if_frequency), + self.__correctFrequency(lo_frequency), + LibHackRfPathFilter(rf_path).value) + # if (result == LibHackRfReturnCode.HACKRF_SUCCESS): + # self.__ifFrequency = if_frequency + # self.__loFrequency = lo_frequency + # self.__rfFilterPath = LibHackRfPathFilter(rf_path) + else: + __class__.__logger.debug("Trying to set frequency with details " + __class__.__name__) + return result + + # def getIntermediateFrequency(self): + # return self.__ifFrequency + + # def getLocalOscillatorFrequency(self): + # return self.__loFrequency + + # def getRfFilterPath(self): + # return self.__rfFilterPath + + # + # This method should be called before setting frequency or baseband as in state + # it acts by modifying the values passed to libhackrf functions + # + def setCrystalPPM(self, ppm): + __class__.__logger.debug("This method must be called before setting frequency or samplerate") + self.__crystalppm = ppm + + def getCrystalPPM(self): + return self.__crystalppm + + def setSampleRate(self, samplerate): + __class__.__logger.debug(__class__.__name__ + " Trying to set samplerate") + result = LibHackRfReturnCode.HACKRF_ERROR_OTHER + if self.opened(): + result = __class__.__libhackrf.hackrf_set_sample_rate(self.__pDevice, self.__correctSampleRate(samplerate)) + # if (result == LibHackRfReturnCode.HACKRF_SUCCESS): + # self.__sampleRate = samplerate + else: + __class__.__logger.debug("Trying to set samplerate " + __class__.__name__) + return result + + def setSampleRateManual(self, samplerate, divider): + __class__.__logger.debug(__class__.__name__ + " Trying to set samplerate") + result = LibHackRfReturnCode.HACKRF_ERROR_OTHER + if self.opened(): + result = __class__.__libhackrf.hackrf_set_sample_rate_manual(self.__pDevice, + self.__correctSampleRate(samplerate), divider) + # if (result == LibHackRfReturnCode.HACKRF_SUCCESS): + # self.__sampleRate = samplerate + else: + __class__.__logger.debug("Trying to set samplerate " + __class__.__name__) + return result diff --git a/realtime-adsb-out.py b/realtime-adsb-out.py new file mode 100755 index 0000000..0a5fc50 --- /dev/null +++ b/realtime-adsb-out.py @@ -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 . +""" + +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 Callsign in hex, Default:0x508035") + print("--callsign Callsign (8 chars max), Default:DEADBEEF") + print("--squawk 4-digits 4096 code squawk, Default:7000") + print("--trajectorytype 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 Latitude for the plane in decimal degrees, Default:50.44994") + print("--long Longitude for the place in decimal degrees. Default:30.5211") + print("--altitude Altitude in decimal feet, Default:1500.0") + print("--speed Airspeed in decimal kph, Default:300.0") + print("--vspeed Vertical speed en ft/min, positive up, Default:0") + print("--maxloadfactor Specify the max load factor for aircraft simulation. Default:1.45") + print("--trackangle Track angle in decimal degrees. Default:0") + print("--timesync 0/1, 0 indicates time not synchronous with UTC, Default:0") + print("--capability Capability, Default:5") + print("--typecode ADS-B message type, Default:11") + print("--sstatus Surveillance status, Default:0") + print("--nicsupplementb NIC supplement-B, Default:0") + print("--surface Aircraft located on ground, Default:False") + print("--waypoints Waypoints file for waypoints trajectory") + print("--posrate 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() \ No newline at end of file diff --git a/test-setup.jpg b/test-setup.jpg new file mode 100755 index 0000000..b112c20 Binary files /dev/null and b/test-setup.jpg differ