├── .gitignore ├── hackrf_antenna_swr_utility ├── __init__.py └── main.py └── readme.md /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | bin/* 3 | lib/* 4 | src/* 5 | man/* 6 | dist/* 7 | docs/build/* 8 | include/* 9 | .Python 10 | *egg* 11 | share/* 12 | .tox/* 13 | -------------------------------------------------------------------------------- /hackrf_antenna_swr_utility/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coddingtonbear/hackrf-antenna-swr-utility/81682ae980ff326216e2deba71067ad9aca72a12/hackrf_antenna_swr_utility/__init__.py -------------------------------------------------------------------------------- /hackrf_antenna_swr_utility/main.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | from __future__ import print_function 3 | 4 | import argparse 5 | import io 6 | import json 7 | import logging 8 | import signal 9 | import sys 10 | import subprocess 11 | 12 | from matplotlib import pyplot 13 | 14 | 15 | COMMANDS = {} 16 | 17 | 18 | logger = logging.getLogger(__name__) 19 | 20 | 21 | def command(fn): 22 | global COMMANDS 23 | COMMANDS[fn.__name__] = fn 24 | 25 | 26 | def get_antenna_statistics(common, start_frequency, stop_frequency): 27 | if common.use_rtl_power: 28 | path = common.rtl_power_path 29 | stats = get_antenna_statistics_rtl_power( 30 | path, start_frequency, stop_frequency, 31 | ) 32 | else: 33 | path = common.osmocom_spectrum_sense_path 34 | stats = get_antenna_statistics_osmocom( 35 | path, start_frequency, stop_frequency 36 | ) 37 | 38 | return stats 39 | 40 | 41 | def get_antenna_statistics_osmocom( 42 | osmocom_path, start_frequency, stop_frequency, loops=5 43 | ): 44 | logger.info( 45 | "Scanning power levels for %s to %s", 46 | start_frequency, 47 | stop_frequency, 48 | ) 49 | try: 50 | # osmocom_spectrum_sense -a hackrf -a repeat=false 750e6 950e6 51 | proc = subprocess.Popen( 52 | [ 53 | osmocom_path, 54 | '--args=hackrf', 55 | '--gain=0', 56 | '%se6' % start_frequency, 57 | '%se6' % stop_frequency, 58 | ], 59 | stdout=subprocess.PIPE, 60 | ) 61 | 62 | frequencies = {} 63 | loop_cursor = 0 64 | max_encountered_frequency = 0 65 | 66 | while True: 67 | line = proc.stdout.readline() 68 | 69 | line = line.strip() 70 | logger.debug(line) 71 | try: 72 | # 2015-08-17 20:32:17.464280 center_freq 765000000.0\ 73 | # freq 767993750.0 power_db 3.53722358601\ 74 | # noise_floor_db -75.9338252561 75 | ( 76 | date, time, 77 | _, center_freq, 78 | _, freq, 79 | _, power_db, 80 | _, noise_floor_db 81 | ) = line.split(' ') 82 | except: 83 | # The first few lines out will always be unusable 84 | continue 85 | 86 | if float(freq) <= max_encountered_frequency: 87 | loop_cursor += 1 88 | else: 89 | max_encountered_frequency = float(freq) 90 | 91 | if loop_cursor >= loops: 92 | logger.info( 93 | "Scanning completed." 94 | ) 95 | proc.send_signal(signal.SIGINT) 96 | break 97 | 98 | frequencies.setdefault(float(center_freq), [])\ 99 | .append(float(power_db)) 100 | except KeyboardInterrupt: 101 | proc.send_signal(signal.SIGINT) 102 | raise 103 | 104 | return frequencies 105 | 106 | 107 | def get_antenna_statistics_rtl_power( 108 | rtl_power_path, start_frequency, stop_frequency, loops=1 109 | ): 110 | logger.info( 111 | "Scanning power levels for %s to %s", 112 | start_frequency, 113 | stop_frequency, 114 | ) 115 | try: 116 | # rtl_power -f 750M:950M:100k 117 | proc = subprocess.Popen( 118 | [ 119 | rtl_power_path, 120 | '-f', 121 | '%sM:%sM:100k' % ( 122 | start_frequency, 123 | stop_frequency, 124 | ) 125 | ], 126 | stdout=subprocess.PIPE, 127 | ) 128 | 129 | frequencies = {} 130 | loop_cursor = 0 131 | max_encountered_frequency = 0 132 | 133 | try: 134 | while True: 135 | line = proc.stdout.readline() 136 | 137 | line = line.strip() 138 | logger.debug(line) 139 | 140 | if not line: 141 | raise StopIteration() 142 | 143 | try: 144 | parts = line.split(',') 145 | base_freq = float(parts[2]) 146 | interval = float(parts[4]) 147 | measurements = parts[6:] 148 | except: 149 | logger.exception( 150 | 'Error encontered while parsing output: %s', 151 | line 152 | ) 153 | continue 154 | 155 | for idx, power in enumerate(measurements): 156 | freq = base_freq + idx * interval 157 | if freq <= max_encountered_frequency: 158 | loop_cursor += 1 159 | else: 160 | max_encountered_frequency = freq 161 | 162 | if loop_cursor >= loops: 163 | logger.info( 164 | "Scanning completed." 165 | ) 166 | proc.send_signal(signal.SIGINT) 167 | raise StopIteration() 168 | 169 | frequencies.setdefault(float(freq), [])\ 170 | .append(float(power)) 171 | except StopIteration: 172 | pass 173 | except KeyboardInterrupt: 174 | proc.send_signal(signal.SIGINT) 175 | 176 | return frequencies 177 | 178 | 179 | def get_frequency_swr_pairs(baseline, measurements): 180 | data_pairs = [] 181 | 182 | for frequency in sorted(baseline.keys()): 183 | pre = baseline[frequency] 184 | post = measurements[float(frequency)] 185 | 186 | pre_value = sum(pre) / float(len(pre)) 187 | post_value = sum(post) / float(len(post)) 188 | 189 | loss = max(pre_value - post_value, 0.001) 190 | swr = (10 ** (loss / 20) + 1) / (10 ** (loss / 20) - 1) 191 | 192 | data_pairs.append((frequency, swr)) 193 | 194 | return data_pairs 195 | 196 | 197 | def display_analysis(data_pairs): 198 | pyplot.subplot(2, 1, 1) 199 | pyplot.plot( 200 | [data[0] for data in data_pairs], 201 | [data[1] for data in data_pairs], 202 | color='blue', 203 | lw=2, 204 | ) 205 | pyplot.yscale('log') 206 | pyplot.show() 207 | 208 | 209 | def read_data_pairs_from_file(path): 210 | pairs = [] 211 | 212 | with io.open(path, 'r') as data: 213 | for line in data: 214 | line = line.strip() 215 | if not line: 216 | continue 217 | 218 | frequency_str, swr_str = line.split(',') 219 | pairs.append( 220 | (float(frequency_str), float(swr_str)) 221 | ) 222 | 223 | return pairs 224 | 225 | 226 | @command 227 | def analyze(common, extra): 228 | parser = argparse.ArgumentParser() 229 | parser.add_argument('baseline') 230 | parser.add_argument('-o', default=None) 231 | parser.add_argument('--stdout', default=False) 232 | parser.add_argument('-i', default=None) 233 | args = parser.parse_args(extra) 234 | 235 | if args.i: 236 | data_pairs = read_data_pairs_from_file(args.i) 237 | else: 238 | with io.open(args.baseline, 'r') as baseline_file: 239 | baseline_data = json.loads(baseline_file.read()) 240 | 241 | statistics = get_antenna_statistics( 242 | common, 243 | baseline_data['meta']['start_frequency'], 244 | baseline_data['meta']['stop_frequency'], 245 | ) 246 | baseline = baseline_data['frequencies'] 247 | 248 | data_pairs = get_frequency_swr_pairs(baseline, statistics) 249 | 250 | if args.o: 251 | if args.stdout: 252 | for frequency, swr in data_pairs: 253 | print("{frequency},{swr}".format(frequency=frequency, swr=swr)) 254 | else: 255 | with io.open(args.o, 'w') as out: 256 | for frequency, swr in data_pairs: 257 | out.write( 258 | "{frequency},{swr}".format( 259 | frequency=frequency, 260 | swr=swr 261 | ) 262 | ) 263 | else: 264 | logger.info("Displaying GUI analysis.") 265 | display_analysis(data_pairs) 266 | 267 | 268 | @command 269 | def baseline(common, extra): 270 | parser = argparse.ArgumentParser() 271 | parser.add_argument('start_frequency', type=int, help='MHz') 272 | parser.add_argument('stop_frequency', type=int, help='MHz') 273 | parser.add_argument('-o', default=None) 274 | args = parser.parse_args(extra) 275 | 276 | statistics = get_antenna_statistics( 277 | common, 278 | args.start_frequency, 279 | args.stop_frequency 280 | ) 281 | 282 | data = { 283 | 'meta': { 284 | 'start_frequency': args.start_frequency, 285 | 'stop_frequency': args.stop_frequency, 286 | }, 287 | 'frequencies': statistics, 288 | } 289 | serialized = json.dumps(data, indent=4, sort_keys=True) 290 | 291 | if args.o: 292 | with io.open(args.o, 'w') as out: 293 | out.write(unicode(serialized)) 294 | else: 295 | print(serialized) 296 | 297 | 298 | def cmdline(args=None): 299 | if args is None: 300 | args = sys.argv[1:] 301 | 302 | parser = argparse.ArgumentParser() 303 | parser.add_argument('command', choices=COMMANDS.keys()) 304 | parser.add_argument( 305 | '--osmocom-spectrum-sense-path', 306 | default='/usr/local/bin/osmocom_spectrum_sense', 307 | ) 308 | parser.add_argument( 309 | '--rtl-power-path', 310 | default='/usr/local/bin/rtl_power', 311 | ) 312 | parser.add_argument( 313 | '--use-rtl-power', 314 | dest='use_rtl_power', 315 | action='store_true', 316 | default=False, 317 | ) 318 | parser.add_argument( 319 | '--loglevel', 320 | default='INFO' 321 | ) 322 | args, extra = parser.parse_known_args(args) 323 | 324 | logging.basicConfig(level=getattr(logging, args.loglevel)) 325 | 326 | COMMANDS[args.command](args, extra) 327 | 328 | 329 | if __name__ == '__main__': 330 | cmdline() 331 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coddingtonbear/hackrf-antenna-swr-utility/81682ae980ff326216e2deba71067ad9aca72a12/readme.md --------------------------------------------------------------------------------