├── heatmap ├── Vera.ttf ├── flatten.py ├── raw_iq.py └── heatmap.py ├── rtl-sdl ├── sdl1.png ├── sdl2.png ├── 8-bit-arch.pcx ├── din1451alt.ttf ├── build.sh ├── rtl_power_lite.c └── waterfall.c └── ais ├── build.sh ├── convenience.h ├── convenience.c └── rtl_ais.c /heatmap/Vera.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/keenerd/rtl-sdr-misc/HEAD/heatmap/Vera.ttf -------------------------------------------------------------------------------- /rtl-sdl/sdl1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/keenerd/rtl-sdr-misc/HEAD/rtl-sdl/sdl1.png -------------------------------------------------------------------------------- /rtl-sdl/sdl2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/keenerd/rtl-sdr-misc/HEAD/rtl-sdl/sdl2.png -------------------------------------------------------------------------------- /rtl-sdl/8-bit-arch.pcx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/keenerd/rtl-sdr-misc/HEAD/rtl-sdl/8-bit-arch.pcx -------------------------------------------------------------------------------- /rtl-sdl/din1451alt.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/keenerd/rtl-sdr-misc/HEAD/rtl-sdl/din1451alt.ttf -------------------------------------------------------------------------------- /ais/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # todo, a real makefile 4 | 5 | files="rtl_ais.c convenience.c" 6 | flags="-Wall -O2" 7 | includes="-I/usr/include/libusb-1.0" 8 | libs="-lusb-1.0 -lrtlsdr -lpthread -lm" 9 | 10 | rm -f rtl_ais 11 | gcc -o rtl_ais $files $flags $includes $libs 12 | 13 | -------------------------------------------------------------------------------- /rtl-sdl/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # todo, a real makefile 4 | 5 | files="waterfall.c" 6 | binary="waterfall" 7 | flags="-Wall -O2" 8 | includes="-I/usr/include/libusb-1.0" 9 | libs="-lSDL -lSDL_image -lSDL_ttf -lusb-1.0 -lrtlsdr -lpthread -lm" 10 | 11 | rm -f $binary 12 | gcc -o $binary $files $flags $includes $libs 13 | 14 | -------------------------------------------------------------------------------- /heatmap/flatten.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | 3 | import sys 4 | from collections import defaultdict 5 | 6 | # todo 7 | # interval based summary 8 | # tall vs wide vs super wide output 9 | 10 | def help(): 11 | print("flatten.py input.csv") 12 | print("turns any rtl_power csv into a more compact summary") 13 | sys.exit() 14 | 15 | if len(sys.argv) <= 1: 16 | help() 17 | 18 | if len(sys.argv) > 2: 19 | help() 20 | 21 | path = sys.argv[1] 22 | 23 | sums = defaultdict(float) 24 | counts = defaultdict(int) 25 | 26 | def frange(start, stop, step): 27 | i = 0 28 | f = start 29 | while f <= stop: 30 | f = start + step*i 31 | yield f 32 | i += 1 33 | 34 | for line in open(path): 35 | line = line.strip().split(', ') 36 | low = int(line[2]) 37 | high = int(line[3]) 38 | step = float(line[4]) 39 | weight = int(line[5]) 40 | dbm = [float(d) for d in line[6:]] 41 | for f,d in zip(frange(low, high, step), dbm): 42 | sums[f] += d*weight 43 | counts[f] += weight 44 | 45 | ave = defaultdict(float) 46 | for f in sums: 47 | ave[f] = sums[f] / counts[f] 48 | 49 | for f in sorted(ave): 50 | print(','.join([str(f), str(ave[f])])) 51 | 52 | 53 | -------------------------------------------------------------------------------- /heatmap/raw_iq.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | 3 | """ 4 | takes raw iq, turns into heatmap 5 | extremely crude, lacks features like windowing 6 | """ 7 | 8 | import sys, math, struct 9 | import numpy 10 | from PIL import Image 11 | 12 | def help(): 13 | print("raw_iq.py bins averages sample-type input.raw") 14 | print(" sample_types: u1 (uint8), s1 (int8), s2 (int16)") 15 | sys.exit() 16 | 17 | def byte_reader(path, sample): 18 | dtype = None 19 | offset = 0 20 | scale = 2**7 21 | if sample == 'u1': 22 | dtype = numpy.uint8 23 | offset = -127 24 | if sample == 's1': 25 | dtype = numpy.int8 26 | if sample == 's2': 27 | dtype = numpy.int16 28 | scale = 2**15 29 | raw = numpy.fromfile(path, dtype).astype(numpy.float64) 30 | raw += offset 31 | raw /= scale 32 | return raw[0::2] + 1j * raw[1::2] 33 | 34 | def psd(data, bin_count, averages): 35 | "really basic, lacks windowing" 36 | length = len(data) 37 | table = [numpy.zeros(bin_count)] 38 | ave = 0 39 | for i in range(0, length, bin_count): 40 | sub_data = numpy.array(data[i:i+bin_count]) 41 | dc_bias = sum(sub_data) / len(sub_data) 42 | #sub_data -= dc_bias 43 | fft = numpy.fft.fft(sub_data) 44 | if len(fft) != bin_count: 45 | continue 46 | table[-1] = table[-1] + numpy.real(numpy.conjugate(fft)*fft) 47 | ave += 1 48 | if ave >= averages: 49 | ave = max(1, ave) 50 | row = table[-1] 51 | row = numpy.concatenate((row[bin_count//2:], row[:bin_count//2])) 52 | # spurious warnings 53 | table[-1] = 10 * numpy.log10(row / ave) 54 | table.append(numpy.zeros(bin_count)) 55 | ave = 0 56 | if ave != 0: 57 | row = table[-1] 58 | row = numpy.concatenate((row[bin_count//2:], row[:bin_count//2])) 59 | table[-1] = 10 * numpy.log10(row / ave) 60 | if ave == 0: 61 | table.pop(-1) 62 | return table 63 | 64 | def rgb2(z, lowest, highest): 65 | g = (z - lowest) / (highest - lowest) 66 | return (int(g*255), int(g*255), 50) 67 | 68 | def heatmap(table): 69 | lowest = -1 70 | highest = -100 71 | for row in table: 72 | lowest = min(lowest, min(z for z in row if not math.isinf(z))) 73 | highest = max(highest, max(row)) 74 | img = Image.new("RGB", (len(table[0]), len(table))) 75 | pix = img.load() 76 | for y,row in enumerate(table): 77 | for x,val in enumerate(row): 78 | if not val >= lowest: # fast nan/-inf test 79 | val = lowest 80 | pix[x,y] = rgb2(val, lowest, highest) 81 | return img 82 | 83 | if __name__ == '__main__': 84 | try: 85 | _, bin_count, averages, sample, path = sys.argv 86 | bin_count = int(bin_count) 87 | bin_count = int(2**(math.ceil(math.log(bin_count, 2)))) 88 | averages = int(averages) 89 | except: 90 | help() 91 | print("loading data") 92 | data = byte_reader(path, sample) 93 | print("estimated size: %i x %i" % (bin_count, 94 | int(len(data) / (bin_count*averages)))) 95 | print("crunching fft") 96 | fft_table = psd(data, bin_count, averages) 97 | print("drawing image") 98 | img = heatmap(fft_table) 99 | print("saving image") 100 | img.save(path + '.png') 101 | 102 | 103 | 104 | -------------------------------------------------------------------------------- /ais/convenience.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2012 by Kyle Keen 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU General Public License as published by 6 | * the Free Software Foundation, either version 2 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU General Public License 15 | * along with this program. If not, see . 16 | */ 17 | 18 | /* a collection of user friendly tools */ 19 | 20 | /*! 21 | * Convert standard suffixes (k, M, G) to double 22 | * 23 | * \param s a string to be parsed 24 | * \return double 25 | */ 26 | 27 | double atofs(char *s); 28 | 29 | /*! 30 | * Convert time suffixes (s, m, h) to double 31 | * 32 | * \param s a string to be parsed 33 | * \return seconds as double 34 | */ 35 | 36 | double atoft(char *s); 37 | 38 | /*! 39 | * Convert percent suffixe (%) to double 40 | * 41 | * \param s a string to be parsed 42 | * \return double 43 | */ 44 | 45 | double atofp(char *s); 46 | 47 | /*! 48 | * Find nearest supported gain 49 | * 50 | * \param dev the device handle given by rtlsdr_open() 51 | * \param target_gain in tenths of a dB 52 | * \return 0 on success 53 | */ 54 | 55 | int nearest_gain(rtlsdr_dev_t *dev, int target_gain); 56 | 57 | /*! 58 | * Set device frequency and report status on stderr 59 | * 60 | * \param dev the device handle given by rtlsdr_open() 61 | * \param frequency in Hz 62 | * \return 0 on success 63 | */ 64 | 65 | int verbose_set_frequency(rtlsdr_dev_t *dev, uint32_t frequency); 66 | 67 | /*! 68 | * Set device sample rate and report status on stderr 69 | * 70 | * \param dev the device handle given by rtlsdr_open() 71 | * \param samp_rate in samples/second 72 | * \return 0 on success 73 | */ 74 | 75 | int verbose_set_sample_rate(rtlsdr_dev_t *dev, uint32_t samp_rate); 76 | 77 | /*! 78 | * Enable or disable the direct sampling mode and report status on stderr 79 | * 80 | * \param dev the device handle given by rtlsdr_open() 81 | * \param on 0 means disabled, 1 I-ADC input enabled, 2 Q-ADC input enabled 82 | * \return 0 on success 83 | */ 84 | 85 | int verbose_direct_sampling(rtlsdr_dev_t *dev, int on); 86 | 87 | /*! 88 | * Enable offset tuning and report status on stderr 89 | * 90 | * \param dev the device handle given by rtlsdr_open() 91 | * \return 0 on success 92 | */ 93 | 94 | int verbose_offset_tuning(rtlsdr_dev_t *dev); 95 | 96 | /*! 97 | * Enable auto gain and report status on stderr 98 | * 99 | * \param dev the device handle given by rtlsdr_open() 100 | * \return 0 on success 101 | */ 102 | 103 | int verbose_auto_gain(rtlsdr_dev_t *dev); 104 | 105 | /*! 106 | * Set tuner gain and report status on stderr 107 | * 108 | * \param dev the device handle given by rtlsdr_open() 109 | * \param gain in tenths of a dB 110 | * \return 0 on success 111 | */ 112 | 113 | int verbose_gain_set(rtlsdr_dev_t *dev, int gain); 114 | 115 | /*! 116 | * Set the frequency correction value for the device and report status on stderr. 117 | * 118 | * \param dev the device handle given by rtlsdr_open() 119 | * \param ppm_error correction value in parts per million (ppm) 120 | * \return 0 on success 121 | */ 122 | 123 | int verbose_ppm_set(rtlsdr_dev_t *dev, int ppm_error); 124 | 125 | 126 | /*! 127 | * Attempts to extract a correction value from eeprom and store it to an int. 128 | * 129 | * \param dev the device handle given by rtlsdr_open() 130 | * \param ppm_error correction value in parts per million (ppm) 131 | * \return 0 on success 132 | */ 133 | int verbose_ppm_eeprom(rtlsdr_dev_t *dev, int *ppm_error); 134 | 135 | /*! 136 | * Reset buffer 137 | * 138 | * \param dev the device handle given by rtlsdr_open() 139 | * \return 0 on success 140 | */ 141 | 142 | int verbose_reset_buffer(rtlsdr_dev_t *dev); 143 | 144 | 145 | /*! 146 | * Find the closest matching device. 147 | * 148 | * \param s a string to be parsed 149 | * \return dev_index int, -1 on error 150 | */ 151 | 152 | int verbose_device_search(char *s); 153 | 154 | -------------------------------------------------------------------------------- /ais/convenience.c: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2012 by Kyle Keen 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU General Public License as published by 6 | * the Free Software Foundation, either version 2 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU General Public License 15 | * along with this program. If not, see . 16 | */ 17 | 18 | /* a collection of user friendly tools 19 | * todo: use strtol for more flexible int parsing 20 | * */ 21 | 22 | #include 23 | #include 24 | #include 25 | 26 | #ifndef _WIN32 27 | #include 28 | #else 29 | #include 30 | #include 31 | #include 32 | #define _USE_MATH_DEFINES 33 | #endif 34 | 35 | #include 36 | 37 | #include "rtl-sdr.h" 38 | 39 | double atofs(char *s) 40 | /* standard suffixes */ 41 | { 42 | char last; 43 | int len; 44 | double suff = 1.0; 45 | len = strlen(s); 46 | last = s[len-1]; 47 | s[len-1] = '\0'; 48 | switch (last) { 49 | case 'g': 50 | case 'G': 51 | suff *= 1e3; 52 | case 'm': 53 | case 'M': 54 | suff *= 1e3; 55 | case 'k': 56 | case 'K': 57 | suff *= 1e3; 58 | suff *= atof(s); 59 | s[len-1] = last; 60 | return suff; 61 | } 62 | s[len-1] = last; 63 | return atof(s); 64 | } 65 | 66 | double atoft(char *s) 67 | /* time suffixes, returns seconds */ 68 | { 69 | char last; 70 | int len; 71 | double suff = 1.0; 72 | len = strlen(s); 73 | last = s[len-1]; 74 | s[len-1] = '\0'; 75 | switch (last) { 76 | case 'h': 77 | case 'H': 78 | suff *= 60; 79 | case 'm': 80 | case 'M': 81 | suff *= 60; 82 | case 's': 83 | case 'S': 84 | suff *= atof(s); 85 | s[len-1] = last; 86 | return suff; 87 | } 88 | s[len-1] = last; 89 | return atof(s); 90 | } 91 | 92 | double atofp(char *s) 93 | /* percent suffixes */ 94 | { 95 | char last; 96 | int len; 97 | double suff = 1.0; 98 | len = strlen(s); 99 | last = s[len-1]; 100 | s[len-1] = '\0'; 101 | switch (last) { 102 | case '%': 103 | suff *= 0.01; 104 | suff *= atof(s); 105 | s[len-1] = last; 106 | return suff; 107 | } 108 | s[len-1] = last; 109 | return atof(s); 110 | } 111 | 112 | int nearest_gain(rtlsdr_dev_t *dev, int target_gain) 113 | { 114 | int i, r, err1, err2, count, nearest; 115 | int* gains; 116 | r = rtlsdr_set_tuner_gain_mode(dev, 1); 117 | if (r < 0) { 118 | fprintf(stderr, "WARNING: Failed to enable manual gain.\n"); 119 | return r; 120 | } 121 | count = rtlsdr_get_tuner_gains(dev, NULL); 122 | if (count <= 0) { 123 | return 0; 124 | } 125 | gains = malloc(sizeof(int) * count); 126 | count = rtlsdr_get_tuner_gains(dev, gains); 127 | nearest = gains[0]; 128 | for (i=0; i=0; i--) { 252 | if (serial[i] != start_char) { 253 | continue;} 254 | fprintf(stderr, "PPM calibration found in eeprom.\n"); 255 | status = 0; 256 | *ppm_error = atoi(serial + i + 1); 257 | break; 258 | } 259 | serial[len-1] = stop_char; 260 | return status; 261 | } 262 | 263 | int verbose_reset_buffer(rtlsdr_dev_t *dev) 264 | { 265 | int r; 266 | r = rtlsdr_reset_buffer(dev); 267 | if (r < 0) { 268 | fprintf(stderr, "WARNING: Failed to reset buffers.\n");} 269 | return r; 270 | } 271 | 272 | int verbose_device_search(char *s) 273 | { 274 | int i, device_count, device, offset; 275 | char *s2; 276 | char vendor[256], product[256], serial[256]; 277 | device_count = rtlsdr_get_device_count(); 278 | if (!device_count) { 279 | fprintf(stderr, "No supported devices found.\n"); 280 | return -1; 281 | } 282 | fprintf(stderr, "Found %d device(s):\n", device_count); 283 | for (i = 0; i < device_count; i++) { 284 | rtlsdr_get_device_usb_strings(i, vendor, product, serial); 285 | fprintf(stderr, " %d: %s, %s, SN: %s\n", i, vendor, product, serial); 286 | } 287 | fprintf(stderr, "\n"); 288 | /* does string look like raw id number */ 289 | device = (int)strtol(s, &s2, 0); 290 | if (s2[0] == '\0' && device >= 0 && device < device_count) { 291 | fprintf(stderr, "Using device %d: %s\n", 292 | device, rtlsdr_get_device_name((uint32_t)device)); 293 | return device; 294 | } 295 | /* does string exact match a serial */ 296 | for (i = 0; i < device_count; i++) { 297 | rtlsdr_get_device_usb_strings(i, vendor, product, serial); 298 | if (strcmp(s, serial) != 0) { 299 | continue;} 300 | device = i; 301 | fprintf(stderr, "Using device %d: %s\n", 302 | device, rtlsdr_get_device_name((uint32_t)device)); 303 | return device; 304 | } 305 | /* does string prefix match a serial */ 306 | for (i = 0; i < device_count; i++) { 307 | rtlsdr_get_device_usb_strings(i, vendor, product, serial); 308 | if (strncmp(s, serial, strlen(s)) != 0) { 309 | continue;} 310 | device = i; 311 | fprintf(stderr, "Using device %d: %s\n", 312 | device, rtlsdr_get_device_name((uint32_t)device)); 313 | return device; 314 | } 315 | /* does string suffix match a serial */ 316 | for (i = 0; i < device_count; i++) { 317 | rtlsdr_get_device_usb_strings(i, vendor, product, serial); 318 | offset = strlen(serial) - strlen(s); 319 | if (offset < 0) { 320 | continue;} 321 | if (strncmp(s, serial+offset, strlen(s)) != 0) { 322 | continue;} 323 | device = i; 324 | fprintf(stderr, "Using device %d: %s\n", 325 | device, rtlsdr_get_device_name((uint32_t)device)); 326 | return device; 327 | } 328 | fprintf(stderr, "No matching devices found.\n"); 329 | return -1; 330 | } 331 | 332 | // vim: tabstop=8:softtabstop=8:shiftwidth=8:noexpandtab 333 | -------------------------------------------------------------------------------- /rtl-sdl/rtl_power_lite.c: -------------------------------------------------------------------------------- 1 | /* 2 | * rtl-sdr, turns your Realtek RTL2832 based DVB dongle into a SDR receiver 3 | * Copyright (C) 2012 by Steve Markgraf 4 | * Copyright (C) 2012 by Hoernchen 5 | * Copyright (C) 2012 by Kyle Keen 6 | * 7 | * This program is free software: you can redistribute it and/or modify 8 | * it under the terms of the GNU General Public License as published by 9 | * the Free Software Foundation, either version 2 of the License, or 10 | * (at your option) any later version. 11 | * 12 | * This program is distributed in the hope that it will be useful, 13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | * GNU General Public License for more details. 16 | * 17 | * You should have received a copy of the GNU General Public License 18 | * along with this program. If not, see . 19 | */ 20 | 21 | // a quick and horrible hack job of rtl_power.c 22 | // 1024 element FFT 23 | // no downsampling 24 | // dedicated thread 25 | // external flags for retune, gain change, data ready, quit 26 | // todo, preface with fft_ 27 | 28 | #include 29 | #include 30 | #include 31 | #include 32 | #include 33 | #include 34 | 35 | #include 36 | 37 | #include 38 | #include 39 | #include 40 | 41 | #include "rtl-sdr.h" 42 | 43 | #define MAX(x, y) (((x) > (y)) ? (x) : (y)) 44 | 45 | #define FFT_LEVEL 10 46 | #define FFT_STACK 4 47 | #define FFT_SIZE (1 << FFT_LEVEL) 48 | #define DEFAULT_BUF_LENGTH (2 * FFT_SIZE * FFT_STACK) 49 | #define BUFFER_DUMP (1<<12) 50 | #define DEFAULT_ASYNC_BUF_NUMBER 32 51 | #define SAMPLE_RATE 3200000 52 | #define PRESCALE 8 53 | #define POSTSCALE 2 54 | #define FREQ_MIN 27000000 55 | #define FREQ_MAX 1700000000 56 | 57 | struct buffer 58 | { 59 | // each buffer should have one writer and one reader thread 60 | // the reader waits for the cond 61 | int16_t buf[DEFAULT_BUF_LENGTH]; 62 | int len; 63 | pthread_rwlock_t rw; 64 | pthread_cond_t ready; 65 | pthread_mutex_t ready_m; 66 | int ready_fast; 67 | }; 68 | 69 | // shared items 70 | 71 | static volatile int do_exit = 0; 72 | static rtlsdr_dev_t *dev = NULL; 73 | static struct buffer fft_out; 74 | static int frequency = 97000000; 75 | 76 | // local items 77 | 78 | struct buffer rtl_out; 79 | struct buffer fft_tmp; 80 | 81 | int16_t* Sinewave; 82 | double* power_table; 83 | int N_WAVE, LOG2_N_WAVE; 84 | int next_power; 85 | int16_t *fft_buf; 86 | int *window_coefs; 87 | 88 | pthread_t dongle_thread; 89 | pthread_t fft_thread; 90 | 91 | #define safe_cond_signal(n, m) pthread_mutex_lock(m); pthread_cond_signal(n); pthread_mutex_unlock(m) 92 | #define safe_cond_wait(n, m) pthread_mutex_lock(m); pthread_cond_wait(n, m); pthread_mutex_unlock(m) 93 | 94 | // some functions from convenience.c 95 | 96 | void gain_default(void) 97 | { 98 | int count; 99 | int* gains; 100 | count = rtlsdr_get_tuner_gains(dev, NULL); 101 | if (count <= 0) 102 | {return;} 103 | gains = malloc(sizeof(int) * count); 104 | count = rtlsdr_get_tuner_gains(dev, gains); 105 | rtlsdr_set_tuner_gain(dev, gains[count-1]); 106 | free(gains); 107 | } 108 | 109 | void gain_increase(void) 110 | { 111 | int i, g, count; 112 | int* gains; 113 | count = rtlsdr_get_tuner_gains(dev, NULL); 114 | if (count <= 0) 115 | {return;} 116 | gains = malloc(sizeof(int) * count); 117 | count = rtlsdr_get_tuner_gains(dev, gains); 118 | g = rtlsdr_get_tuner_gain(dev); 119 | for (i=0; i<(count-1); i++) 120 | { 121 | if (gains[i] == g) 122 | { 123 | rtlsdr_set_tuner_gain(dev, gains[i+1]); 124 | break; 125 | } 126 | } 127 | free(gains); 128 | } 129 | 130 | void gain_decrease(void) 131 | { 132 | int i, g, count; 133 | int* gains; 134 | count = rtlsdr_get_tuner_gains(dev, NULL); 135 | if (count <= 0) 136 | {return;} 137 | gains = malloc(sizeof(int) * count); 138 | count = rtlsdr_get_tuner_gains(dev, gains); 139 | g = rtlsdr_get_tuner_gain(dev); 140 | for (i=1; i FREQ_MAX) 156 | {frequency = FREQ_MAX;} 157 | rtlsdr_set_center_freq(dev, frequency); 158 | } 159 | 160 | // fft stuff 161 | 162 | void sine_table(int size) 163 | { 164 | int i; 165 | double d; 166 | LOG2_N_WAVE = size; 167 | N_WAVE = 1 << LOG2_N_WAVE; 168 | Sinewave = malloc(sizeof(int16_t) * N_WAVE*3/4); 169 | power_table = malloc(sizeof(double) * N_WAVE); 170 | for (i=0; i> 14; 181 | b = c & 0x01; 182 | return (c >> 1) + b; 183 | } 184 | 185 | int fix_fft(int16_t iq[], int m) 186 | /* interleaved iq[], 0 <= n < 2**m, changes in place */ 187 | { 188 | int mr, nn, i, j, l, k, istep, n, shift; 189 | int16_t qr, qi, tr, ti, wr, wi; 190 | n = 1 << m; 191 | if (n > N_WAVE) 192 | {return -1;} 193 | mr = 0; 194 | nn = n - 1; 195 | /* decimation in time - re-order data */ 196 | for (m=1; m<=nn; ++m) { 197 | l = n; 198 | do 199 | {l >>= 1;} 200 | while (mr+l > nn); 201 | mr = (mr & (l-1)) + l; 202 | if (mr <= m) 203 | {continue;} 204 | // real = 2*m, imag = 2*m+1 205 | tr = iq[2*m]; 206 | iq[2*m] = iq[2*mr]; 207 | iq[2*mr] = tr; 208 | ti = iq[2*m+1]; 209 | iq[2*m+1] = iq[2*mr+1]; 210 | iq[2*mr+1] = ti; 211 | } 212 | l = 1; 213 | k = LOG2_N_WAVE-1; 214 | while (l < n) { 215 | shift = 1; 216 | istep = l << 1; 217 | for (m=0; m>= 1; wi >>= 1;} 223 | for (i=m; i>= 1; qi >>= 1;} 231 | iq[2*j] = qr - tr; 232 | iq[2*j+1] = qi - ti; 233 | iq[2*i] = qr + tr; 234 | iq[2*i+1] = qi + ti; 235 | } 236 | } 237 | --k; 238 | l = istep; 239 | } 240 | return 0; 241 | } 242 | 243 | void remove_dc(int16_t *data, int length) 244 | /* works on interleaved data */ 245 | { 246 | int i; 247 | int16_t ave; 248 | long sum = 0L; 249 | for (i=0; i < length; i+=2) { 250 | sum += data[i]; 251 | } 252 | ave = (int16_t)(sum / (long)(length)); 253 | if (ave == 0) { 254 | return;} 255 | for (i=0; i < length; i+=2) { 256 | data[i] -= ave; 257 | } 258 | } 259 | 260 | int32_t real_conj(int16_t real, int16_t imag) 261 | /* real(n * conj(n)) */ 262 | { 263 | return ((int32_t)real*(int32_t)real + (int32_t)imag*(int32_t)imag); 264 | } 265 | 266 | // threading stuff 267 | 268 | void rtl_callback_fn(unsigned char *buf, uint32_t len, void *ctx) 269 | { 270 | int i; 271 | if (do_exit) 272 | {return;} 273 | pthread_rwlock_wrlock(&rtl_out.rw); 274 | for (i=0; irw, NULL); 341 | pthread_cond_init(&buf->ready, NULL); 342 | pthread_mutex_init(&buf->ready_m, NULL); 343 | return 0; 344 | } 345 | 346 | int buffer_cleanup(struct buffer* buf) 347 | { 348 | pthread_rwlock_destroy(&buf->rw); 349 | pthread_cond_destroy(&buf->ready); 350 | pthread_mutex_destroy(&buf->ready_m); 351 | return 0; 352 | } 353 | 354 | static int fft_launch(void) 355 | { 356 | sine_table(FFT_LEVEL); 357 | 358 | buffer_init(&rtl_out); 359 | buffer_init(&fft_tmp); 360 | buffer_init(&fft_out); 361 | 362 | rtlsdr_open(&dev, 0); // todo, verbose_device_search() 363 | 364 | // settings 365 | rtlsdr_reset_buffer(dev); 366 | rtlsdr_set_center_freq(dev, frequency); 367 | rtlsdr_set_sample_rate(dev, SAMPLE_RATE); 368 | rtlsdr_set_tuner_gain_mode(dev, 1); 369 | gain_default(); 370 | 371 | pthread_create(&dongle_thread, NULL, &dongle_thread_fn, NULL); 372 | pthread_create(&fft_thread, NULL, &fft_thread_fn, NULL); 373 | return 0; 374 | } 375 | 376 | static int fft_cleanup(void) 377 | { 378 | do_exit = 1; 379 | usleep(10000); 380 | rtlsdr_cancel_async(dev); 381 | pthread_join(dongle_thread, NULL); 382 | safe_cond_signal(&rtl_out.ready, &rtl_out.ready_m); 383 | pthread_join(fft_thread, NULL); 384 | safe_cond_signal(&fft_out.ready, &fft_out.ready_m); 385 | 386 | rtlsdr_close(dev); 387 | 388 | buffer_cleanup(&rtl_out); 389 | buffer_cleanup(&fft_tmp); 390 | buffer_cleanup(&fft_out); 391 | 392 | return 0; 393 | } 394 | 395 | // vim: tabstop=4:softtabstop=4:shiftwidth=4:expandtab 396 | -------------------------------------------------------------------------------- /rtl-sdl/waterfall.c: -------------------------------------------------------------------------------- 1 | 2 | /* 3 | 4 | SDL powered waterfall 5 | 6 | at the moment this everything is hard-coded for a single platform 7 | the BeagleboneBlack with an LCD7 touchscreen (framebuffer mode) 8 | 9 | it can run on other platforms, but will not autodetect anything 10 | the keybinds are laid out for the touchscreen face buttons 11 | 12 | on the BBB: 13 | full screen double buffered blits seem to perform at 140 fps (cpu limited) 14 | 15 | to run automatically: 16 | @reboot sleep 1 && cd /the/install/path && ./waterfall 17 | 18 | todo: 19 | benchmark against fftw3 20 | replace defines with options 21 | autodetect things like screen resolution 22 | change the displayed bandwidth 23 | audio demodulation 24 | fix screen blanking 25 | a real make file 26 | 27 | */ 28 | 29 | #include 30 | #include 31 | 32 | #include "SDL/SDL.h" 33 | #include "SDL/SDL_image.h" 34 | #include "SDL/SDL_ttf.h" 35 | 36 | #include "rtl_power_lite.c" 37 | 38 | #define SCREEN_WIDTH 800 39 | #define SCREEN_HEIGHT 480 40 | char* font_path = "./din1451alt.ttf"; 41 | #define FONT_SIZE 24 42 | #define FRAME_MS 30 43 | #define FRAME_LINES 10 44 | #define MAX_STRING 100 45 | #define BIG_JUMP 50000000 46 | 47 | #if SDL_BYTEORDER == SDL_BIG_ENDIAN 48 | static const Uint32 r_mask = 0xFF000000; 49 | static const Uint32 g_mask = 0x00FF0000; 50 | static const Uint32 b_mask = 0x0000FF00; 51 | static const Uint32 a_mask = 0x000000FF; 52 | #else 53 | static const Uint32 r_mask = 0x000000FF; 54 | static const Uint32 g_mask = 0x0000FF00; 55 | static const Uint32 b_mask = 0x00FF0000; 56 | static const Uint32 a_mask = 0xFF000000; 57 | #endif 58 | 59 | static SDL_Surface* img_surface; 60 | static SDL_Surface* scroll_surface; 61 | static SDL_Surface* future_surface; 62 | static const SDL_VideoInfo* info = 0; 63 | SDL_Surface* screen; 64 | TTF_Font *font; 65 | int do_flip; // todo, cond 66 | int credits_toggle; 67 | 68 | struct text_bin 69 | { 70 | char string[MAX_STRING]; 71 | int x, y; 72 | int i; 73 | int dirty; 74 | SDL_Surface* surf_fg; 75 | SDL_Surface* surf_bg; 76 | }; 77 | 78 | struct text_bin credits[6]; 79 | struct text_bin freq_labels[5]; 80 | 81 | int init_video() 82 | { 83 | if (SDL_Init(SDL_INIT_VIDEO) < 0) 84 | { 85 | fprintf(stderr, "Video initialization failed: %s\n", 86 | SDL_GetError()); 87 | return 0; 88 | } 89 | 90 | info = SDL_GetVideoInfo(); 91 | 92 | if( !info ) { 93 | fprintf( stderr, "Video query failed: %s\n", 94 | SDL_GetError( ) ); 95 | return 0; 96 | } 97 | 98 | return 1; 99 | } 100 | 101 | int set_video( Uint16 width, Uint16 height, int bpp, int flags) 102 | { 103 | if (init_video()) 104 | { 105 | if((screen = SDL_SetVideoMode(width,height,bpp,flags))==0) 106 | { 107 | fprintf( stderr, "Video mode set failed: %s\n", 108 | SDL_GetError( ) ); 109 | return 0; 110 | } 111 | } 112 | return 1; 113 | } 114 | 115 | int init_ttf() 116 | { 117 | if (TTF_Init() != 0) 118 | { 119 | fprintf( stderr, "TTF init failed: %s\n", 120 | SDL_GetError( ) ); 121 | return 1; 122 | } 123 | font = TTF_OpenFont(font_path, FONT_SIZE); 124 | if (font == NULL) 125 | { 126 | fprintf( stderr, "TTF load failed: %s\n", 127 | TTF_GetError( ) ); 128 | return 1; 129 | } 130 | return 0; 131 | } 132 | 133 | void quit( int code ) 134 | { 135 | SDL_FreeSurface(scroll_surface); 136 | SDL_FreeSurface(future_surface); 137 | SDL_FreeSurface(img_surface); 138 | 139 | TTF_Quit( ); 140 | SDL_Quit( ); 141 | 142 | exit( code ); 143 | } 144 | 145 | void handle_key_down(SDL_keysym* keysym) 146 | { 147 | switch(keysym->sym) 148 | { 149 | case SDLK_ESCAPE: 150 | quit(0); 151 | break; 152 | case SDLK_RETURN: 153 | credits_toggle = !credits_toggle; 154 | break; 155 | case SDLK_DOWN: 156 | case SDLK_UP: 157 | case SDLK_LEFT: 158 | case SDLK_RIGHT: 159 | default: 160 | break; 161 | } 162 | } 163 | 164 | void process_events( void ) 165 | { 166 | SDL_Event event; 167 | 168 | while( SDL_PollEvent( &event ) ) { 169 | 170 | switch( event.type ) { 171 | case SDL_KEYDOWN: 172 | handle_key_down( &event.key.keysym ); 173 | break; 174 | case SDL_QUIT: 175 | quit( 0 ); 176 | break; 177 | } 178 | } 179 | } 180 | 181 | void init() 182 | { 183 | SDL_Surface* tmp; 184 | int i; 185 | SDL_Color colors[256]; 186 | 187 | tmp = SDL_CreateRGBSurface(SDL_HWSURFACE, SCREEN_WIDTH, 188 | SCREEN_HEIGHT, 8, r_mask, g_mask, b_mask, a_mask); 189 | scroll_surface = SDL_DisplayFormat(tmp); 190 | SDL_FreeSurface(tmp); 191 | 192 | tmp = SDL_CreateRGBSurface(SDL_HWSURFACE, SCREEN_WIDTH, 193 | SCREEN_HEIGHT, 8, r_mask, g_mask, b_mask, a_mask); 194 | future_surface = SDL_DisplayFormat(tmp); 195 | SDL_FreeSurface(tmp); 196 | 197 | img_surface = IMG_Load("8-bit-arch.pcx"); 198 | for (i = 0; i < SDL_NUMEVENTS; ++i) 199 | { 200 | if (i != SDL_KEYDOWN && i != SDL_QUIT) 201 | { 202 | SDL_EventState(i, SDL_IGNORE); 203 | } 204 | } 205 | 206 | for(i=0; i<256; i++) 207 | { 208 | colors[i].r = i; 209 | colors[i].g = i; 210 | colors[i].b = 50; 211 | } 212 | colors[0].r = 0; colors[0].g = 0; colors[0].b = 0; 213 | colors[255].r = 255; colors[255].g = 255; colors[255].b = 255; 214 | 215 | SDL_SetPalette(future_surface, SDL_LOGPAL|SDL_PHYSPAL, colors, 0, 256); 216 | 217 | SDL_ShowCursor(SDL_DISABLE); 218 | } 219 | 220 | void putpixel(SDL_Surface *surface, int x, int y, uint32_t pixel) 221 | /* taken from some stackoverflow post */ 222 | { 223 | int bpp = surface->format->BytesPerPixel; 224 | /* Here p is the address to the pixel we want to set */ 225 | Uint8 *p = (Uint8 *)surface->pixels + y * surface->pitch + x * bpp; 226 | 227 | switch (bpp) { 228 | case 1: 229 | *p = pixel; 230 | break; 231 | 232 | case 2: 233 | *(uint16_t *)p = pixel; 234 | break; 235 | 236 | case 3: 237 | if (SDL_BYTEORDER == SDL_BIG_ENDIAN) { 238 | p[0] = (pixel >> 16) & 0xff; 239 | p[1] = (pixel >> 8) & 0xff; 240 | p[2] = pixel & 0xff; 241 | } 242 | else { 243 | p[0] = pixel & 0xff; 244 | p[1] = (pixel >> 8) & 0xff; 245 | p[2] = (pixel >> 16) & 0xff; 246 | } 247 | break; 248 | 249 | case 4: 250 | *(uint32_t *)p = pixel; 251 | break; 252 | 253 | default: 254 | break; /* shouldn't happen, but avoids warnings */ 255 | } 256 | } 257 | 258 | int pretty_text(SDL_Surface* surface, struct text_bin* text) 259 | { 260 | SDL_Color fg_color = {255, 255, 255}; 261 | SDL_Color bg_color = {0, 0, 0}; 262 | SDL_Rect fg_rect = {text->x + 0, text->y + 0, SCREEN_WIDTH, SCREEN_HEIGHT}; 263 | SDL_Rect bg_rect = {text->x + 2, text->y + 2, SCREEN_WIDTH, SCREEN_HEIGHT}; 264 | 265 | if (text->dirty) 266 | { 267 | // this leaks, but freeing segfaults? 268 | // in practice, it leaks an MB an hour under very heavy use 269 | //if (text->surf_fg != NULL) 270 | // {SDL_FreeSurface(text->surf_fg);} 271 | //if (text->surf_bg != NULL) 272 | // {SDL_FreeSurface(text->surf_bg);} 273 | text->surf_fg = TTF_RenderText_Solid(font, text->string, fg_color); 274 | text->surf_bg = TTF_RenderText_Solid(font, text->string, bg_color); 275 | text->dirty = 0; 276 | } 277 | 278 | SDL_BlitSurface(text->surf_bg, NULL, surface, &bg_rect); 279 | SDL_BlitSurface(text->surf_fg, NULL, surface, &fg_rect); 280 | 281 | return 0; 282 | } 283 | 284 | void build_credits(void) 285 | { 286 | int i; 287 | int xs[] = {300, 300, 300, 300, 300, 300}; 288 | int ys[] = {100, 150, 200, 250, 300, 350}; 289 | strncpy(credits[0].string, "board: BeagleBone Black", MAX_STRING); 290 | strncpy(credits[1].string, "display: CircuitCo LCD7", MAX_STRING); 291 | strncpy(credits[2].string, "radio: rtl-sdr", MAX_STRING); 292 | strncpy(credits[3].string, "graphics: SDL", MAX_STRING); 293 | strncpy(credits[4].string, "os: Arch Linux ARM", MAX_STRING); 294 | strncpy(credits[5].string, "glue: Kyle Keen", MAX_STRING); 295 | for (i=0; i<6; i++) 296 | { 297 | credits[i].x = xs[i]; 298 | credits[i].y = ys[i]; 299 | credits[i].dirty = 1; 300 | } 301 | } 302 | 303 | void show_credits(SDL_Surface* surface) 304 | { 305 | int i; 306 | for (i=0; i<6; i++) 307 | {pretty_text(surface, &(credits[i]));} 308 | } 309 | 310 | void build_labels(void) 311 | { 312 | // very similar to the lines code 313 | int f, i, x, drift, center; 314 | drift = (frequency % 1000000) / (SAMPLE_RATE / FFT_SIZE); 315 | center = frequency - (frequency % 1000000); 316 | for (i=-2; i<=2; i++) 317 | { 318 | x = SCREEN_WIDTH / 2 + -drift + i * 1000000 / (SAMPLE_RATE / FFT_SIZE); 319 | f = center + i * 1000000; 320 | freq_labels[i+2].x = x - FONT_SIZE/2; 321 | freq_labels[i+2].y = 10; 322 | if (freq_labels[i+2].i == f) 323 | {continue;} 324 | freq_labels[i+2].dirty = 1; 325 | freq_labels[i+2].i = f; 326 | snprintf(freq_labels[i+2].string, MAX_STRING, "%i", f/1000000); 327 | } 328 | } 329 | 330 | void static_events(void) 331 | { 332 | SDL_Rect blank = {0, 0, SCREEN_WIDTH, SCREEN_HEIGHT}; 333 | uint8_t* keystate = SDL_GetKeyState(NULL); 334 | if (keystate[SDLK_LEFT]) 335 | { 336 | frequency -= BIG_JUMP; 337 | frequency_set(); 338 | build_labels(); 339 | SDL_FillRect(scroll_surface, &blank, 0); 340 | } 341 | if (keystate[SDLK_RIGHT]) 342 | { 343 | frequency += BIG_JUMP; 344 | frequency_set(); 345 | build_labels(); 346 | SDL_FillRect(scroll_surface, &blank, 0); 347 | } 348 | if (keystate[SDLK_UP]) 349 | {gain_decrease();} 350 | if (keystate[SDLK_DOWN]) 351 | {gain_increase();} 352 | } 353 | 354 | uint32_t frame_callback(uint32_t interval, void* param) 355 | { 356 | do_flip = 1; 357 | return interval; 358 | } 359 | 360 | uint32_t rgb(uint32_t i) 361 | { 362 | return ((b_mask/255)*20 | (r_mask/255)*i | (g_mask/255)*i); 363 | } 364 | 365 | int mouse_stuff(void) 366 | // returns X scroll offset 367 | // kind of crap with variable framerate 368 | { 369 | static double prev_x = -100; 370 | static double velo = 0; 371 | double deaccel = 10; 372 | int x, y, buttons; 373 | buttons = SDL_GetMouseState(&x, &y); 374 | if (buttons & SDL_BUTTON_LMASK) 375 | { 376 | if (prev_x < 0) 377 | { 378 | prev_x = x; 379 | } 380 | velo = x - prev_x; 381 | prev_x = x; 382 | //fprintf(stdout, "%i %f\n", x, velo); 383 | } else { 384 | prev_x = -100; 385 | if (velo > deaccel) 386 | {velo -= deaccel;} 387 | if (velo < -deaccel) 388 | {velo += deaccel;} 389 | if (velo >= -deaccel && velo <= deaccel) 390 | {velo *= 0.5;} 391 | } 392 | return (int)round(velo); 393 | } 394 | 395 | int main( int argc, char* argv[] ) 396 | { 397 | int i, c, x, y, v, line; 398 | int blits = 0; 399 | uint32_t pixel = 0; 400 | struct text_bin text; 401 | SDL_Rect ScrollFrom = {0, 1, SCREEN_WIDTH, SCREEN_HEIGHT}; 402 | if (!set_video(SCREEN_WIDTH, SCREEN_HEIGHT, 8, 403 | SDL_HWSURFACE | SDL_HWACCEL | SDL_HWPALETTE /*| SDL_FULLSCREEN*/)) 404 | quit(1); 405 | init_ttf(); 406 | //SDL_Init(SDL_INIT_TIMER); 407 | 408 | SDL_WM_SetCaption("Demo", ""); 409 | 410 | init(); 411 | 412 | build_credits(); 413 | build_labels(); 414 | 415 | strncpy(text.string, "<< >> - + ?", MAX_STRING); 416 | text.x = 30; 417 | text.y = 450; 418 | text.dirty = 1; 419 | 420 | SDL_BlitSurface(img_surface, NULL, scroll_surface, NULL); 421 | //SDL_AddTimer(FRAME_MS, frame_callback, NULL); 422 | 423 | fft_launch(); 424 | y = 0; 425 | SDL_LockSurface(future_surface); 426 | while(1) 427 | { 428 | process_events(); 429 | //safe_cond_wait(&fft_out.ready, &fft_out.ready_m); 430 | if (!fft_out.ready_fast) 431 | { 432 | usleep(1000); 433 | continue; 434 | } 435 | fft_out.ready_fast = 0; 436 | pthread_rwlock_rdlock(&fft_out.rw); 437 | for (x=0; x 254) 443 | {c = 254;} 444 | if (c < 1) 445 | {c = 1;} 446 | //fprintf(stdout, "%i ", fft_out.buf[i]); 447 | putpixel(future_surface, x, y, 40*fft_out.buf[i] + 1); 448 | pixel++; 449 | } 450 | // lines every 100KHz 451 | line = (frequency % 100000) / (SAMPLE_RATE / FFT_SIZE); 452 | for (i=-15; i<15; i++) 453 | { 454 | if (y%4) 455 | {break;} 456 | x = SCREEN_WIDTH / 2 + -line + i * 100000 / (SAMPLE_RATE / FFT_SIZE); 457 | if (x < 0) 458 | {continue;} 459 | if (x > SCREEN_WIDTH) 460 | {continue;} 461 | putpixel(future_surface, x, y, 0xFF); 462 | } 463 | //fprintf(stdout, "\n"); 464 | pthread_rwlock_unlock(&fft_out.rw); 465 | y++; 466 | if (!do_flip && y <= FRAME_LINES) 467 | {continue;} 468 | static_events(); 469 | v = mouse_stuff(); 470 | if (v != 0) 471 | { 472 | frequency += (-v * SAMPLE_RATE / FFT_SIZE); 473 | frequency_set(); 474 | build_labels(); 475 | } 476 | SDL_UnlockSurface(future_surface); 477 | // scroll 478 | ScrollFrom.x = -v; 479 | ScrollFrom.y = y; 480 | ScrollFrom.w = SCREEN_WIDTH; 481 | ScrollFrom.h = SCREEN_HEIGHT; 482 | SDL_BlitSurface(scroll_surface, &ScrollFrom, scroll_surface, NULL); 483 | // nuke edges 484 | if (v > 0) 485 | { 486 | ScrollFrom.x = 0; 487 | ScrollFrom.y = 0; 488 | } 489 | if (v < 0) 490 | { 491 | ScrollFrom.x = SCREEN_WIDTH+v; 492 | ScrollFrom.y = 0; 493 | } 494 | if (v != 0) 495 | { 496 | ScrollFrom.w = abs(v); 497 | ScrollFrom.h = SCREEN_HEIGHT-y; 498 | SDL_FillRect(scroll_surface, &ScrollFrom, 0); 499 | } 500 | // new stuff 501 | ScrollFrom.x = v; 502 | ScrollFrom.y = SCREEN_HEIGHT - y; 503 | ScrollFrom.w = SCREEN_WIDTH; 504 | ScrollFrom.h = SCREEN_HEIGHT; 505 | SDL_BlitSurface(future_surface, NULL, scroll_surface, &ScrollFrom); 506 | SDL_BlitSurface(scroll_surface, NULL, screen, NULL); 507 | // overlay 508 | pretty_text(screen, &text); 509 | if (credits_toggle) 510 | {show_credits(screen);} 511 | for (i=0; i<5; i++) 512 | {pretty_text(screen, &freq_labels[i]);} 513 | pretty_text(screen, &text); 514 | SDL_Flip(screen); 515 | // only way to keep the BBB from blanking the screen 516 | // (the 10 minute timeout can not be changed by any known means) 517 | if (blits % 2000 == 0) 518 | {system("setterm -blank poke");} 519 | blits++; 520 | do_flip = 0; 521 | y = 0; 522 | SDL_LockSurface(future_surface); 523 | } 524 | quit(0); 525 | fft_cleanup(); 526 | 527 | return 0; 528 | } 529 | 530 | 531 | // vim:set tabstop=4 softtabstop=4 shiftwidth=4 expandtab smarttab: 532 | -------------------------------------------------------------------------------- /ais/rtl_ais.c: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2012 by Kyle Keen 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU General Public License as published by 6 | * the Free Software Foundation, either version 2 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU General Public License 15 | * along with this program. If not, see . 16 | */ 17 | 18 | 19 | /* todo 20 | * support left > right 21 | * thread left/right channels 22 | * more array sharing 23 | * something to correct for clock drift (look at demod's dc bias?) 24 | * 4x oversampling (with cic up/down) 25 | * droop correction 26 | * alsa integration 27 | * better upsampler (libsamplerate?) 28 | * windows support 29 | * ais decoder 30 | */ 31 | 32 | #include 33 | #include 34 | #include 35 | #include 36 | #include 37 | #include 38 | 39 | #include 40 | 41 | #include 42 | #include 43 | 44 | #include 45 | #include "convenience.h" 46 | 47 | #define DEFAULT_ASYNC_BUF_NUMBER 12 48 | #define DEFAULT_BUF_LENGTH (16 * 16384) 49 | #define AUTO_GAIN -100 50 | 51 | static pthread_t demod_thread; 52 | static pthread_cond_t ready; 53 | static pthread_mutex_t ready_m; 54 | static volatile int do_exit = 0; 55 | static rtlsdr_dev_t *dev = NULL; 56 | 57 | /* todo, less globals */ 58 | int16_t *merged; 59 | int merged_len; 60 | FILE *file; 61 | int oversample = 0; 62 | int dc_filter = 1; 63 | 64 | /* signals are not threadsafe by default */ 65 | #define safe_cond_signal(n, m) pthread_mutex_lock(m); pthread_cond_signal(n); pthread_mutex_unlock(m) 66 | #define safe_cond_wait(n, m) pthread_mutex_lock(m); pthread_cond_wait(n, m); pthread_mutex_unlock(m) 67 | 68 | struct downsample_state 69 | { 70 | int16_t *buf; 71 | int len_in; 72 | int len_out; 73 | int rate_in; 74 | int rate_out; 75 | int downsample; 76 | int downsample_passes; 77 | int16_t lp_i_hist[10][6]; 78 | int16_t lp_q_hist[10][6]; 79 | pthread_rwlock_t rw; 80 | }; 81 | 82 | struct demod_state 83 | { 84 | int16_t *buf; 85 | int buf_len; 86 | int16_t *result; 87 | int result_len; 88 | int now_r, now_j; 89 | int pre_r, pre_j; 90 | int dc_avg; // really should get its own struct 91 | }; 92 | 93 | struct upsample_stereo 94 | { 95 | int16_t *buf_left; 96 | int16_t *buf_right; 97 | int16_t *result; 98 | int bl_len; 99 | int br_len; 100 | int result_len; 101 | int rate; 102 | }; 103 | 104 | /* complex iq pairs */ 105 | struct downsample_state both; 106 | struct downsample_state left; 107 | struct downsample_state right; 108 | /* iq pairs and real mono */ 109 | struct demod_state left_demod; 110 | struct demod_state right_demod; 111 | /* real stereo pairs (upsampled) */ 112 | struct upsample_stereo stereo; 113 | 114 | void usage(void) 115 | { 116 | fprintf(stderr, 117 | "rtl_ais, a simple AIS tuner\n" 118 | "\t and generic dual-frequency FM demodulator\n\n" 119 | "(probably not a good idea to use with e4000 tuners)\n" 120 | "Use: rtl_ais [options] [outputfile]\n" 121 | "\t[-l left_frequency (default: 161.975M)]\n" 122 | "\t[-r right_frequency (default: 162.025M)]\n" 123 | "\t left freq < right freq\n" 124 | "\t frequencies must be within 1.2MHz\n" 125 | "\t[-s sample_rate (default: 12k)]\n" 126 | "\t minimum value, might be up to 2x specified\n" 127 | "\t[-o output_rate (default: 48k)]\n" 128 | "\t must be equal or greater than twice -s value\n" 129 | "\t[-E toggle edge tuning (default: off)]\n" 130 | "\t[-D toggle DC filter (default: on)]\n" 131 | //"\t[-O toggle oversampling (default: off)\n" 132 | "\t[-d device_index (default: 0)]\n" 133 | "\t[-g tuner_gain (default: automatic)]\n" 134 | "\t[-p ppm_error (default: 0)]\n" 135 | "\tfilename (a '-' dumps samples to stdout)\n" 136 | "\t omitting the filename also uses stdout\n\n" 137 | "\tOutput is stereo 2x16 bit signed ints\n\n" 138 | "rtl_ais | play -t raw -r 48k -es -b 16 -c 2 -V1 -\n" 139 | "\n"); 140 | exit(1); 141 | } 142 | 143 | static void sighandler(int signum) 144 | { 145 | fprintf(stderr, "Signal caught, exiting!\n"); 146 | do_exit = 1; 147 | rtlsdr_cancel_async(dev); 148 | } 149 | 150 | void rotate_90(int16_t *buf, int len) 151 | /* 90 rotation is 1+0j, 0+1j, -1+0j, 0-1j 152 | or [0, 1, -3, 2, -4, -5, 7, -6] */ 153 | { 154 | int i; 155 | int16_t tmp; 156 | for (i=0; i> 4; 203 | for (i=4; i> 4; 211 | } 212 | /* archive */ 213 | hist[0] = a; 214 | hist[1] = b; 215 | hist[2] = c; 216 | hist[3] = d; 217 | hist[4] = e; 218 | hist[5] = f; 219 | } 220 | 221 | void downsample(struct downsample_state *d) 222 | { 223 | int i, ds_p; 224 | ds_p = d->downsample_passes; 225 | for (i=0; ibuf, (d->len_in >> i), d->lp_i_hist[i]); 227 | fifth_order(d->buf+1, (d->len_in >> i)-1, d->lp_q_hist[i]); 228 | } 229 | } 230 | 231 | void multiply(int ar, int aj, int br, int bj, int *cr, int *cj) 232 | { 233 | *cr = ar*br - aj*bj; 234 | *cj = aj*br + ar*bj; 235 | } 236 | 237 | int polar_discriminant(int ar, int aj, int br, int bj) 238 | { 239 | int cr, cj; 240 | double angle; 241 | multiply(ar, aj, br, -bj, &cr, &cj); 242 | angle = atan2((double)cj, (double)cr); 243 | return (int)(angle / 3.14159 * (1<<14)); 244 | } 245 | 246 | int fast_atan2(int y, int x) 247 | /* pre scaled for int16 */ 248 | { 249 | int yabs, angle; 250 | int pi4=(1<<12), pi34=3*(1<<12); // note pi = 1<<14 251 | if (x==0 && y==0) { 252 | return 0; 253 | } 254 | yabs = y; 255 | if (yabs < 0) { 256 | yabs = -yabs; 257 | } 258 | if (x >= 0) { 259 | angle = pi4 - pi4 * (x-yabs) / (x+yabs); 260 | } else { 261 | angle = pi34 - pi4 * (x+yabs) / (yabs-x); 262 | } 263 | if (y < 0) { 264 | return -angle; 265 | } 266 | return angle; 267 | } 268 | 269 | int polar_disc_fast(int ar, int aj, int br, int bj) 270 | { 271 | int cr, cj; 272 | multiply(ar, aj, br, -bj, &cr, &cj); 273 | return fast_atan2(cj, cr); 274 | } 275 | 276 | void demodulate(struct demod_state *d) 277 | { 278 | int i, pcm; 279 | int16_t *buf = d->buf; 280 | int16_t *result = d->result; 281 | pcm = polar_disc_fast(buf[0], buf[1], 282 | d->pre_r, d->pre_j); 283 | result[0] = (int16_t)pcm; 284 | for (i = 2; i < (d->buf_len-1); i += 2) { 285 | // add the other atan types? 286 | pcm = polar_disc_fast(buf[i], buf[i+1], 287 | buf[i-2], buf[i-1]); 288 | result[i/2] = (int16_t)pcm; 289 | } 290 | d->pre_r = buf[d->buf_len - 2]; 291 | d->pre_j = buf[d->buf_len - 1]; 292 | } 293 | 294 | void dc_block_filter(struct demod_state *d) 295 | { 296 | int i, avg; 297 | int64_t sum = 0; 298 | int16_t *result = d->result; 299 | for (i=0; i < d->result_len; i++) { 300 | sum += result[i]; 301 | } 302 | avg = sum / d->result_len; 303 | avg = (avg + d->dc_avg * 9) / 10; 304 | for (i=0; i < d->result_len; i++) { 305 | result[i] -= avg; 306 | } 307 | d->dc_avg = avg; 308 | } 309 | 310 | void arbitrary_upsample(int16_t *buf1, int16_t *buf2, int len1, int len2) 311 | /* linear interpolation, len1 < len2 */ 312 | { 313 | int i = 1; 314 | int j = 0; 315 | int tick = 0; 316 | double frac; // use integers... 317 | while (j < len2) { 318 | frac = (double)tick / (double)len2; 319 | buf2[j] = (int16_t)((double)buf1[i-1]*(1-frac) + (double)buf1[i]*frac); 320 | j++; 321 | tick += len1; 322 | if (tick > len2) { 323 | tick -= len2; 324 | i++; 325 | } 326 | if (i >= len1) { 327 | i = len1 - 1; 328 | tick = len2; 329 | } 330 | } 331 | } 332 | 333 | static void rtlsdr_callback(unsigned char *buf, uint32_t len, void *ctx) 334 | { 335 | int i; 336 | if (do_exit) { 337 | return;} 338 | pthread_rwlock_wrlock(&both.rw); 339 | for (i=0; ibuf = malloc(dss->len_in * sizeof(int16_t)); 394 | dss->rate_out = dss->rate_in / dss->downsample; 395 | //dss->downsample_passes = (int)log2(dss->downsample); 396 | dss->len_out = dss->len_in / dss->downsample; 397 | for (i=0; i<10; i++) { for (j=0; j<6; j++) { 398 | dss->lp_i_hist[i][j] = 0; 399 | dss->lp_q_hist[i][j] = 0; 400 | }} 401 | pthread_rwlock_init(&dss->rw, NULL); 402 | } 403 | 404 | void demod_init(struct demod_state *ds) 405 | { 406 | ds->buf = malloc(ds->buf_len * sizeof(int16_t)); 407 | ds->result = malloc(ds->result_len * sizeof(int16_t)); 408 | } 409 | 410 | void stereo_init(struct upsample_stereo *us) 411 | { 412 | us->buf_left = malloc(us->bl_len * sizeof(int16_t)); 413 | us->buf_right = malloc(us->br_len * sizeof(int16_t)); 414 | us->result = malloc(us->result_len * sizeof(int16_t)); 415 | } 416 | 417 | int main(int argc, char **argv) 418 | { 419 | struct sigaction sigact; 420 | char *filename = NULL; 421 | int r, opt; 422 | int i, gain = AUTO_GAIN; /* tenths of a dB */ 423 | int dev_index = 0; 424 | int dev_given = 0; 425 | int ppm_error = 0; 426 | int custom_ppm = 0; 427 | int left_freq = 161975000; 428 | int right_freq = 162025000; 429 | int sample_rate = 12000; 430 | int output_rate = 48000; 431 | int dongle_freq, dongle_rate, delta; 432 | int edge = 0; 433 | pthread_cond_init(&ready, NULL); 434 | pthread_mutex_init(&ready_m, NULL); 435 | 436 | while ((opt = getopt(argc, argv, "l:r:s:o:EODd:g:p:h")) != -1) 437 | { 438 | switch (opt) { 439 | case 'l': 440 | left_freq = (int)atofs(optarg); 441 | break; 442 | case 'r': 443 | right_freq = (int)atofs(optarg); 444 | break; 445 | case 's': 446 | sample_rate = (int)atofs(optarg); 447 | break; 448 | case 'o': 449 | output_rate = (int)atofs(optarg); 450 | break; 451 | case 'E': 452 | edge = !edge; 453 | break; 454 | case 'D': 455 | dc_filter = !dc_filter; 456 | break; 457 | case 'O': 458 | oversample = !oversample; 459 | break; 460 | case 'd': 461 | dev_index = verbose_device_search(optarg); 462 | dev_given = 1; 463 | break; 464 | case 'g': 465 | gain = (int)(atof(optarg) * 10); 466 | break; 467 | case 'p': 468 | ppm_error = atoi(optarg); 469 | custom_ppm = 1; 470 | break; 471 | case 'h': 472 | default: 473 | usage(); 474 | return 2; 475 | } 476 | } 477 | 478 | if (argc <= optind) { 479 | filename = "-"; 480 | } else { 481 | filename = argv[optind]; 482 | } 483 | 484 | if (left_freq > right_freq) { 485 | usage(); 486 | return 2; 487 | } 488 | 489 | /* precompute rates */ 490 | dongle_freq = left_freq/2 + right_freq/2; 491 | if (edge) { 492 | dongle_freq -= sample_rate/2;} 493 | delta = right_freq - left_freq; 494 | if (delta > 1.2e6) { 495 | fprintf(stderr, "Frequencies may be at most 1.2MHz apart."); 496 | exit(1); 497 | } 498 | if (delta < 0) { 499 | fprintf(stderr, "Left channel must be lower than right channel."); 500 | exit(1); 501 | } 502 | i = (int)log2(2.4e6 / delta); 503 | dongle_rate = delta * (1< output_rate) { 521 | fprintf(stderr, "Channel bandwidth too high or output bandwidth too low."); 522 | exit(1); 523 | } 524 | 525 | stereo.rate = output_rate; 526 | 527 | if (edge) { 528 | fprintf(stderr, "Edge tuning enabled.\n"); 529 | } else { 530 | fprintf(stderr, "Edge tuning disabled.\n"); 531 | } 532 | if (dc_filter) { 533 | fprintf(stderr, "DC filter enabled.\n"); 534 | } else { 535 | fprintf(stderr, "DC filter disabled.\n"); 536 | } 537 | fprintf(stderr, "Buffer size: %0.2f mS\n", 1000 * (double)DEFAULT_BUF_LENGTH / (double)dongle_rate); 538 | fprintf(stderr, "Downsample factor: %i\n", both.downsample * left.downsample); 539 | fprintf(stderr, "Low pass: %i Hz\n", left.rate_out); 540 | fprintf(stderr, "Output: %i Hz\n", output_rate); 541 | 542 | /* precompute lengths */ 543 | both.len_in = DEFAULT_BUF_LENGTH; 544 | both.len_out = both.len_in / both.downsample; 545 | left.len_in = both.len_out; 546 | right.len_in = both.len_out; 547 | left.len_out = left.len_in / left.downsample; 548 | right.len_out = right.len_in / right.downsample; 549 | left_demod.buf_len = left.len_out; 550 | left_demod.result_len = left_demod.buf_len / 2; 551 | right_demod.buf_len = left_demod.buf_len; 552 | right_demod.result_len = left_demod.result_len; 553 | stereo.bl_len = (int)((long)(DEFAULT_BUF_LENGTH/2) * (long)output_rate / (long)dongle_rate); 554 | stereo.br_len = stereo.bl_len; 555 | stereo.result_len = stereo.br_len * 2; 556 | stereo.rate = output_rate; 557 | 558 | if (!dev_given) { 559 | dev_index = verbose_device_search("0"); 560 | } 561 | 562 | if (dev_index < 0) { 563 | exit(1); 564 | } 565 | 566 | downsample_init(&both); 567 | downsample_init(&left); 568 | downsample_init(&right); 569 | demod_init(&left_demod); 570 | demod_init(&right_demod); 571 | stereo_init(&stereo); 572 | 573 | r = rtlsdr_open(&dev, (uint32_t)dev_index); 574 | if (r < 0) { 575 | fprintf(stderr, "Failed to open rtlsdr device #%d.\n", dev_index); 576 | exit(1); 577 | } 578 | sigact.sa_handler = sighandler; 579 | sigemptyset(&sigact.sa_mask); 580 | sigact.sa_flags = 0; 581 | sigaction(SIGINT, &sigact, NULL); 582 | sigaction(SIGTERM, &sigact, NULL); 583 | sigaction(SIGQUIT, &sigact, NULL); 584 | sigaction(SIGPIPE, &sigact, NULL); 585 | 586 | if (strcmp(filename, "-") == 0) { /* Write samples to stdout */ 587 | file = stdout; 588 | setvbuf(stdout, NULL, _IONBF, 0); 589 | } else { 590 | file = fopen(filename, "wb"); 591 | if (!file) { 592 | fprintf(stderr, "Failed to open %s\n", filename); 593 | exit(1); 594 | } 595 | } 596 | 597 | /* Set the tuner gain */ 598 | if (gain == AUTO_GAIN) { 599 | verbose_auto_gain(dev); 600 | } else { 601 | gain = nearest_gain(dev, gain); 602 | verbose_gain_set(dev, gain); 603 | } 604 | 605 | if (!custom_ppm) { 606 | verbose_ppm_eeprom(dev, &ppm_error); 607 | } 608 | verbose_ppm_set(dev, ppm_error); 609 | //r = rtlsdr_set_agc_mode(dev, 1); 610 | 611 | /* Set the tuner frequency */ 612 | verbose_set_frequency(dev, dongle_freq); 613 | 614 | /* Set the sample rate */ 615 | verbose_set_sample_rate(dev, dongle_rate); 616 | 617 | /* Reset endpoint before we start reading from it (mandatory) */ 618 | verbose_reset_buffer(dev); 619 | 620 | pthread_create(&demod_thread, NULL, demod_thread_fn, (void *)(NULL)); 621 | rtlsdr_read_async(dev, rtlsdr_callback, (void *)(NULL), 622 | DEFAULT_ASYNC_BUF_NUMBER, 623 | DEFAULT_BUF_LENGTH); 624 | 625 | if (do_exit) { 626 | fprintf(stderr, "\nUser cancel, exiting...\n");} 627 | else { 628 | fprintf(stderr, "\nLibrary error %d, exiting...\n", r);} 629 | rtlsdr_cancel_async(dev); 630 | safe_cond_signal(&ready, &ready_m); 631 | pthread_cond_destroy(&ready); 632 | pthread_mutex_destroy(&ready_m); 633 | 634 | if (file != stdout) { 635 | fclose(file);} 636 | 637 | rtlsdr_close(dev); 638 | return r >= 0 ? r : -r; 639 | } 640 | 641 | // vim: tabstop=8:softtabstop=8:shiftwidth=8:noexpandtab 642 | -------------------------------------------------------------------------------- /heatmap/heatmap.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | 3 | from PIL import Image, ImageDraw, ImageFont 4 | import os, sys, gzip, math, argparse, colorsys, datetime 5 | from collections import defaultdict 6 | from itertools import * 7 | 8 | urlretrieve = lambda a, b: None 9 | try: 10 | import urllib.request 11 | urlretrieve = urllib.request.urlretrieve 12 | except: 13 | import urllib 14 | urlretrieve = urllib.urlretrieve 15 | 16 | # todo: 17 | # matplotlib powered --interactive 18 | # arbitrary freq marker spacing 19 | # ppm 20 | # blue-less marker grid 21 | # fast summary thing 22 | # gain normalization 23 | # check pil version for brokenness 24 | 25 | vera_url = "https://github.com/keenerd/rtl-sdr-misc/raw/master/heatmap/Vera.ttf" 26 | vera_path = os.path.join(sys.path[0], "Vera.ttf") 27 | 28 | tape_height = 25 29 | tape_pt = 10 30 | 31 | if not os.path.isfile(vera_path): 32 | urlretrieve(vera_url, vera_path) 33 | 34 | try: 35 | font = ImageFont.truetype(vera_path, 10) 36 | except: 37 | print('Please download the Vera.ttf font and place it in the current directory.') 38 | sys.exit(1) 39 | 40 | def build_parser(): 41 | parser = argparse.ArgumentParser(description='Convert rtl_power CSV files into graphics.') 42 | parser.add_argument('input_path', metavar='INPUT', type=str, 43 | help='Input CSV file. (may be a .csv.gz)') 44 | parser.add_argument('output_path', metavar='OUTPUT', type=str, 45 | help='Output image. (various extensions supported)') 46 | parser.add_argument('--offset', dest='offset_freq', default=None, 47 | help='Shift the entire frequency range, for up/down converters.') 48 | parser.add_argument('--ytick', dest='time_tick', default=None, 49 | help='Place ticks along the Y axis every N seconds/minutes/hours/days.') 50 | parser.add_argument('--db', dest='db_limit', nargs=2, default=None, 51 | help='Minimum and maximum db values.') 52 | parser.add_argument('--compress', dest='compress', default=0, 53 | help='Apply a gradual asymptotic time compression. Values > 1 are the new target height, values < 1 are a scaling factor.') 54 | slicegroup = parser.add_argument_group('Slicing', 55 | 'Efficiently render a portion of the data. (optional) Frequencies can take G/M/k suffixes. Timestamps look like "YYYY-MM-DD HH:MM:SS" Durations take d/h/m/s suffixes.') 56 | slicegroup.add_argument('--low', dest='low_freq', default=None, 57 | help='Minimum frequency for a subrange.') 58 | slicegroup.add_argument('--high', dest='high_freq', default=None, 59 | help='Maximum frequency for a subrange.') 60 | slicegroup.add_argument('--begin', dest='begin_time', default=None, 61 | help='Timestamp to start at.') 62 | slicegroup.add_argument('--end', dest='end_time', default=None, 63 | help='Timestamp to stop at.') 64 | slicegroup.add_argument('--head', dest='head_time', default=None, 65 | help='Duration to use, starting at the beginning.') 66 | slicegroup.add_argument('--tail', dest='tail_time', default=None, 67 | help='Duration to use, stopping at the end.') 68 | parser.add_argument('--palette', dest='palette', default='default', 69 | help='Set Color Palette: default, extended, charolastra, twente') 70 | return parser 71 | 72 | def frange(start, stop, step): 73 | i = 0 74 | while (i*step + start <= stop): 75 | yield i*step + start 76 | i += 1 77 | 78 | def min_filter(row): 79 | size = 3 80 | result = [] 81 | for i in range(size): 82 | here = row[i] 83 | near = row[0:i] + row[i+1:size] 84 | if here > min(near): 85 | result.append(here) 86 | continue 87 | result.append(min(near)) 88 | for i in range(size-1, len(row)): 89 | here = row[i] 90 | near = row[i-(size-1):i] 91 | if here > min(near): 92 | result.append(here) 93 | continue 94 | result.append(min(near)) 95 | return result 96 | 97 | def floatify(zs): 98 | # nix errors with -inf, windows errors with -1.#J 99 | zs2 = [] 100 | previous = 0 # awkward for single-column rows 101 | for z in zs: 102 | try: 103 | z = float(z) 104 | except ValueError: 105 | z = previous 106 | if math.isinf(z): 107 | z = previous 108 | if math.isnan(z): 109 | z = previous 110 | zs2.append(z) 111 | previous = z 112 | return zs2 113 | 114 | def freq_parse(s): 115 | suffix = 1 116 | if s.lower().endswith('k'): 117 | suffix = 1e3 118 | if s.lower().endswith('m'): 119 | suffix = 1e6 120 | if s.lower().endswith('g'): 121 | suffix = 1e9 122 | if suffix != 1: 123 | s = s[:-1] 124 | return float(s) * suffix 125 | 126 | def duration_parse(s): 127 | suffix = 1 128 | if s.lower().endswith('s'): 129 | suffix = 1 130 | if s.lower().endswith('m'): 131 | suffix = 60 132 | if s.lower().endswith('h'): 133 | suffix = 60 * 60 134 | if s.lower().endswith('d'): 135 | suffix = 24 * 60 * 60 136 | if suffix != 1 or s.lower().endswith('s'): 137 | s = s[:-1] 138 | return float(s) * suffix 139 | 140 | def date_parse(s): 141 | if '-' not in s: 142 | return datetime.datetime.fromtimestamp(int(s)) 143 | return datetime.datetime.strptime(s, '%Y-%m-%d %H:%M:%S') 144 | 145 | def palette_parse(s): 146 | palettes = {'default': default_palette, 147 | 'extended': extended_palette, 148 | 'charolastra': charolastra_palette, 149 | 'twente': twente_palette, 150 | } 151 | if s not in palettes: 152 | print('WARNING: %s not a valid palette' % s) 153 | return palettes.get(s, default_palette) 154 | 155 | def gzip_wrap(path): 156 | "hides silly CRC errors" 157 | iterator = gzip.open(path, 'rb') 158 | running = True 159 | while running: 160 | try: 161 | line = next(iterator) 162 | if type(line) == bytes: 163 | line = line.decode('utf-8') 164 | yield line 165 | except IOError: 166 | running = False 167 | 168 | def time_compression(y, decay): 169 | return int(round((1/decay)*math.exp(y*decay) - 1/decay)) 170 | 171 | def reparse(args, label, fn): 172 | if args.__getattribute__(label) is None: 173 | return 174 | args.__setattr__(label, fn(args.__getattribute__(label))) 175 | 176 | def prepare_args(): 177 | # hack, http://stackoverflow.com/questions/9025204/ 178 | for i, arg in enumerate(sys.argv): 179 | if (arg[0] == '-') and arg[1].isdigit(): 180 | sys.argv[i] = ' ' + arg 181 | parser = build_parser() 182 | args = parser.parse_args() 183 | 184 | reparse(args, 'low_freq', freq_parse) 185 | reparse(args, 'high_freq', freq_parse) 186 | reparse(args, 'offset_freq', freq_parse) 187 | if args.offset_freq is None: 188 | args.offset_freq = 0 189 | reparse(args, 'time_tick', duration_parse) 190 | reparse(args, 'begin_time', date_parse) 191 | reparse(args, 'end_time', date_parse) 192 | reparse(args, 'head_time', duration_parse) 193 | reparse(args, 'tail_time', duration_parse) 194 | reparse(args, 'palette', palette_parse) 195 | reparse(args, 'head_time', lambda s: datetime.timedelta(seconds=s)) 196 | reparse(args, 'tail_time', lambda s: datetime.timedelta(seconds=s)) 197 | args.compress = float(args.compress) 198 | 199 | if args.db_limit: 200 | a,b = args.db_limit 201 | args.db_limit = (float(a), float(b)) 202 | 203 | if args.begin_time and args.tail_time: 204 | print("Can't combine --begin and --tail") 205 | sys.exit(2) 206 | if args.end_time and args.head_time: 207 | print("Can't combine --end and --head") 208 | sys.exit(2) 209 | if args.head_time and args.tail_time: 210 | print("Can't combine --head and --tail") 211 | sys.exit(2) 212 | return args 213 | 214 | def open_raw_data(path): 215 | raw_data = lambda: open(path) 216 | if path.endswith('.gz'): 217 | raw_data = lambda: gzip_wrap(path) 218 | return raw_data 219 | 220 | def slice_columns(columns, low_freq, high_freq): 221 | start_col = 0 222 | stop_col = len(columns) 223 | if low_freq is not None and low <= low_freq <= high: 224 | start_col = sum(f args.end_time: 258 | break 259 | times.add(t) 260 | columns = list(frange(low, high, step)) 261 | start_col, stop_col = slice_columns(columns, args.low_freq, args.high_freq) 262 | f_key = (columns[start_col], columns[stop_col], step) 263 | zs = line[6+start_col:6+stop_col+1] 264 | if not zs: 265 | continue 266 | if f_key not in f_cache: 267 | freq2 = list(frange(*f_key))[:len(zs)] 268 | freqs.update(freq2) 269 | #freqs.add(f_key[1]) # high 270 | #labels.add(f_key[0]) # low 271 | f_cache.add(f_key) 272 | 273 | if not args.db_limit: 274 | zs = floatify(zs) 275 | min_z = min(min_z, min(zs)) 276 | max_z = max(max_z, max(zs)) 277 | 278 | if start is None: 279 | start = date_parse(t) 280 | stop = date_parse(t) 281 | if args.head_time is not None and args.end_time is None: 282 | args.end_time = start + args.head_time 283 | 284 | if not args.db_limit: 285 | args.db_limit = (min_z, max_z) 286 | 287 | if args.tail_time is not None: 288 | times = [t for t in times if date_parse(t) >= (stop - args.tail_time)] 289 | start = date_parse(min(times)) 290 | 291 | freqs = list(sorted(list(freqs))) 292 | times = list(sorted(list(times))) 293 | labels = list(sorted(list(labels))) 294 | 295 | if len(labels) == 1: 296 | delta = (max(freqs) - min(freqs)) / (len(freqs) / 500.0) 297 | delta = round(delta / 10**int(math.log10(delta))) * 10**int(math.log10(delta)) 298 | delta = int(delta) 299 | lower = int(math.ceil(min(freqs) / delta) * delta) 300 | labels = list(range(lower, int(max(freqs)), delta)) 301 | 302 | height = len(times) 303 | pix_height = height 304 | if args.compress: 305 | if args.compress > height: 306 | args.compress = 0 307 | print("Image too short, disabling time compression") 308 | if 0 < args.compress < 1: 309 | args.compress *= height 310 | if args.compress: 311 | args.compress = -1 / args.compress 312 | pix_height = time_compression(height, args.compress) 313 | 314 | print("x: %i, y: %i, z: (%f, %f)" % (len(freqs), pix_height, args.db_limit[0], args.db_limit[1])) 315 | args.freqs = freqs 316 | args.times = times 317 | args.labels = labels 318 | args.pix_height = pix_height 319 | args.start_stop = (start, stop) 320 | args.pixel_bandwidth = step 321 | 322 | def default_palette(): 323 | return [(i, i, 50) for i in range(256)] 324 | 325 | def extended_palette(): 326 | p = [(0,0,50)] 327 | for i in range(1, 256): 328 | p.append((i, i-1, 50)) 329 | p.append((i-1, i, 50)) 330 | p.append((i, i, 50)) 331 | return p 332 | 333 | def charolastra_palette(): 334 | p = [] 335 | for i in range(1024): 336 | g = i / 1023.0 337 | c = colorsys.hsv_to_rgb(0.65-(g-0.08), 1, 0.2+g) 338 | p.append((int(c[0]*256), int(c[1]*256), int(c[2]*256))) 339 | return p 340 | 341 | def twente_palette(): 342 | p = [] 343 | for i in range(20, 100, 2): 344 | p.append((0, 0, i)) 345 | for i in range(256): 346 | g = i / 255.0 347 | p.append((int(g*255), 0, int(g*155)+100)) 348 | for i in range(256): 349 | p.append((255, i, 255)) 350 | # intentionally blow out the highs 351 | for i in range(100): 352 | p.append((255, 255, 255)) 353 | return p 354 | 355 | def rgb_fn(palette, min_z, max_z): 356 | "palette is a list of tuples, returns a function of z" 357 | def rgb_inner(z): 358 | tone = (z - min_z) / (max_z - min_z) 359 | tone_scaled = int(tone * (len(palette)-1)) 360 | return palette[tone_scaled] 361 | return rgb_inner 362 | 363 | def collate_row(x_size): 364 | # this is more fragile than the old code 365 | # sensitive to timestamps that are out of order 366 | old_t = None 367 | row = [0.0] * x_size 368 | for line in raw_data(): 369 | line = [s.strip() for s in line.strip().split(',')] 370 | #line = [line[0], line[1]] + [float(s) for s in line[2:] if s] 371 | line = [s for s in line if s] 372 | t = line[0] + ' ' + line[1] 373 | if '-' not in line[0]: 374 | t = line[0] 375 | if t not in args.times: 376 | continue # happens with live files and time cropping 377 | if old_t is None: 378 | old_t = t 379 | low = int(line[2]) + args.offset_freq 380 | high = int(line[3]) + args.offset_freq 381 | step = float(line[4]) 382 | columns = list(frange(low, high, step)) 383 | start_col, stop_col = slice_columns(columns, args.low_freq, args.high_freq) 384 | if args.low_freq and columns[stop_col] < args.low_freq: 385 | continue 386 | if args.high_freq and columns[start_col] > args.high_freq: 387 | continue 388 | start_freq = columns[start_col] 389 | if args.low_freq: 390 | start_freq = max(args.low_freq, start_freq) 391 | # sometimes fails? skip or abort? 392 | x_start = args.freqs.index(start_freq) 393 | zs = floatify(line[6+start_col:6+stop_col+1]) 394 | if t != old_t: 395 | yield old_t, row 396 | row = [0.0] * x_size 397 | old_t = t 398 | for i in range(len(zs)): 399 | x = x_start + i 400 | if x >= x_size: 401 | continue 402 | row[x] = zs[i] 403 | yield old_t, row 404 | 405 | def push_pixels(args): 406 | "returns PIL img" 407 | width = len(args.freqs) 408 | rgb = rgb_fn(args.palette(), args.db_limit[0], args.db_limit[1]) 409 | img = Image.new("RGB", (width, tape_height + args.pix_height + 1)) 410 | pix = img.load() 411 | x_size = img.size[0] 412 | average = [0.0] * width 413 | tally = 0 414 | old_y = None 415 | height = len(args.times) 416 | for t, zs in collate_row(x_size): 417 | y = args.times.index(t) 418 | if not args.compress: 419 | for x in range(len(zs)): 420 | pix[x,y+tape_height+1] = rgb(zs[x]) 421 | continue 422 | # ugh 423 | y = args.pix_height - time_compression(height - y, args.compress) 424 | if old_y is None: 425 | old_y = y 426 | if old_y != y: 427 | for x in range(len(average)): 428 | pix[x,old_y+tape_height+1] = rgb(average[x]/tally) 429 | tally = 0 430 | average = [0.0] * width 431 | old_y = y 432 | for x in range(len(zs)): 433 | average[x] += zs[x] 434 | tally += 1 435 | return img 436 | 437 | def closest_index(n, m_list, interpolate=False): 438 | "assumes sorted m_list, returns two points for interpolate" 439 | i = len(m_list) // 2 440 | jump = len(m_list) // 2 441 | while jump > 1: 442 | i_down = i - jump 443 | i_here = i 444 | i_up = i + jump 445 | if i_down < 0: 446 | i_down = i 447 | if i_up >= len(m_list): 448 | i_up = i 449 | e_down = abs(m_list[i_down] - n) 450 | e_here = abs(m_list[i_here] - n) 451 | e_up = abs(m_list[i_up] - n) 452 | e_best = min([e_down, e_here, e_up]) 453 | if e_down == e_best: 454 | i = i_down 455 | if e_up == e_best: 456 | i = i_up 457 | if e_here == e_best: 458 | i = i_here 459 | jump = jump // 2 460 | if not interpolate: 461 | return i 462 | if n < m_list[i] and i > 0: 463 | return i-1, i 464 | if n > m_list[i] and i < len(m_list)-1: 465 | return i, i+1 466 | return i, i 467 | 468 | def word_aa(label, pt, fg_color, bg_color): 469 | f = ImageFont.truetype(vera_path, pt*3) 470 | s = f.getsize(label) 471 | s = (s[0], pt*3 + 3) # getsize lies, manually compute 472 | w_img = Image.new("RGB", s, bg_color) 473 | w_draw = ImageDraw.Draw(w_img) 474 | w_draw.text((0, 0), label, font=f, fill=fg_color) 475 | return w_img.resize((s[0]//3, s[1]//3), Image.ANTIALIAS) 476 | 477 | def blend(percent, c1, c2): 478 | "c1 and c2 are RGB tuples" 479 | # probably isn't gamma correct 480 | r = c1[0] * percent + c2[0] * (1 - percent) 481 | g = c1[1] * percent + c2[1] * (1 - percent) 482 | b = c1[2] * percent + c2[2] * (1 - percent) 483 | c3 = map(int, map(round, [r,g,b])) 484 | return tuple(c3) 485 | 486 | def tape_lines(draw, freqs, interval, y1, y2, used=set()): 487 | min_f = min(freqs) 488 | max_f = max(freqs) 489 | "returns the number of lines" 490 | low_f = (min_f // interval) * interval 491 | high_f = (1 + max_f // interval) * interval 492 | hits = 0 493 | blur = lambda p: blend(p, (255, 255, 0), (0, 0, 0)) 494 | for i in range(int(low_f), int(high_f), int(interval)): 495 | if not (min_f < i < max_f): 496 | continue 497 | hits += 1 498 | if i in used: 499 | continue 500 | x1,x2 = closest_index(i, args.freqs, interpolate=True) 501 | if x1 == x2: 502 | draw.line([x1,y1,x1,y2], fill='black') 503 | else: 504 | percent = (i - args.freqs[x1]) / float(args.freqs[x2] - args.freqs[x1]) 505 | draw.line([x1,y1,x1,y2], fill=blur(percent)) 506 | draw.line([x2,y1,x2,y2], fill=blur(1-percent)) 507 | used.add(i) 508 | return hits 509 | 510 | def tape_text(img, freqs, interval, y, used=set()): 511 | min_f = min(freqs) 512 | max_f = max(freqs) 513 | low_f = (min_f // interval) * interval 514 | high_f = (1 + max_f // interval) * interval 515 | for i in range(int(low_f), int(high_f), int(interval)): 516 | if i in used: 517 | continue 518 | if not (min_f < i < max_f): 519 | continue 520 | x = closest_index(i, freqs) 521 | s = str(i) 522 | if interval >= 1e6: 523 | s = '%iM' % (i/1e6) 524 | elif interval > 1000: 525 | s = '%ik' % ((i/1e3) % 1000) 526 | if s.startswith('0'): 527 | s = '%iM' % (i/1e6) 528 | else: 529 | s = '%i' % (i%1000) 530 | if s.startswith('0'): 531 | s = '%ik' % ((i/1e3) % 1000) 532 | if s.startswith('0'): 533 | s = '%iM' % (i/1e6) 534 | w = word_aa(s, tape_pt, 'black', 'yellow') 535 | img.paste(w, (x - w.size[0]//2, y)) 536 | used.add(i) 537 | 538 | def shadow_text(draw, x, y, s, font, fg_color='white', bg_color='black'): 539 | draw.text((x+1, y+1), s, font=font, fill=bg_color) 540 | draw.text((x, y), s, font=font, fill=fg_color) 541 | 542 | def create_labels(args, img): 543 | draw = ImageDraw.Draw(img) 544 | font = ImageFont.load_default() 545 | pixel_bandwidth = args.pixel_bandwidth 546 | 547 | draw.rectangle([0,0,img.size[0],tape_height], fill='yellow') 548 | min_freq = min(args.freqs) 549 | max_freq = max(args.freqs) 550 | delta = max_freq - min_freq 551 | width = len(args.freqs) 552 | height = len(args.times) 553 | label_base = 9 554 | 555 | for i in range(label_base, 0, -1): 556 | interval = int(10**i) 557 | low_f = (min_freq // interval) * interval 558 | high_f = (1 + max_freq // interval) * interval 559 | hits = len(range(int(low_f), int(high_f), interval)) 560 | if hits >= 4: 561 | label_base = i 562 | break 563 | label_base = 10**label_base 564 | 565 | for scale,y in [(1,10), (5,15), (10,19), (50,22), (100,24), (500, 25)]: 566 | hits = tape_lines(draw, args.freqs, label_base/scale, y, tape_height) 567 | pixels_per_hit = width / hits 568 | if pixels_per_hit > 50: 569 | tape_text(img, args.freqs, label_base/scale, y-tape_pt) 570 | if pixels_per_hit < 10: 571 | break 572 | 573 | start, stop = args.start_stop 574 | duration = stop - start 575 | duration = duration.days * 24*60*60 + duration.seconds + 30 576 | pixel_height = duration / len(args.times) 577 | hours = int(duration / 3600) 578 | minutes = int((duration - 3600*hours) / 60) 579 | 580 | if args.time_tick: 581 | label_format = "%H:%M:%S" 582 | if args.time_tick % (60*60*24) == 0: 583 | label_format = "%Y-%m-%d" 584 | elif args.time_tick % 60 == 0: 585 | label_format = "%H:%M" 586 | label_next = datetime.datetime(start.year, start.month, start.day, start.hour) 587 | tick_delta = datetime.timedelta(seconds = args.time_tick) 588 | while label_next < start: 589 | label_next += tick_delta 590 | last_y = -100 591 | full_height = args.pix_height 592 | for y,t in enumerate(args.times): 593 | label_time = date_parse(t) 594 | if label_time < label_next: 595 | continue 596 | if args.compress: 597 | y = full_height - time_compression(height - y, args.compress) 598 | if y - last_y > 15: 599 | shadow_text(draw, 2, y+tape_height, label_next.strftime(label_format), font) 600 | last_y = y 601 | label_next += tick_delta 602 | 603 | margin = 2 604 | if args.time_tick: 605 | margin = 60 606 | shadow_text(draw, margin, img.size[1] - 45, 'Duration: %i:%02i' % (hours, minutes), font) 607 | shadow_text(draw, margin, img.size[1] - 35, 'Range: %.2fMHz - %.2fMHz' % (min_freq/1e6, (max_freq+pixel_bandwidth)/1e6), font) 608 | shadow_text(draw, margin, img.size[1] - 25, 'Pixel: %.2fHz x %is' % (pixel_bandwidth, int(round(pixel_height))), font) 609 | shadow_text(draw, margin, img.size[1] - 15, 'Started: {0}'.format(start), font) 610 | # bin size 611 | 612 | print("loading") 613 | args = prepare_args() 614 | raw_data = open_raw_data(args.input_path) 615 | summarize_pass(args) 616 | 617 | print("drawing") 618 | img = push_pixels(args) 619 | 620 | print("labeling") 621 | create_labels(args, img) 622 | 623 | print("saving") 624 | img.save(args.output_path) 625 | 626 | --------------------------------------------------------------------------------