├── directTDoA_static_server_list.db ├── icon.gif ├── maps ├── directTDoA_map.jpg ├── directTDoA_map_grayscale_with_sea.jpg └── How to find the other maps.txt ├── .gitignore ├── .idea ├── vcs.xml ├── modules.xml ├── misc.xml ├── directTDoA.iml └── directTDoA4.iml ├── .gitmodules ├── kiwiwavreader_patch.diff ├── json_save_patch.diff ├── Copying.wtfpl ├── read_data_patch.diff ├── kiwiclient_patch.diff ├── modsocket_patch.diff ├── nognss.py ├── setup.sh ├── kiwiworker_patch.diff ├── plot_map_patch.diff ├── plot_iq.py ├── directTDoA.cfg ├── getmap.py ├── kiwirecorder_patch.diff ├── trim_iq.py ├── microkiwi_patch.diff ├── README.md ├── directTDoA_server_list.db └── compute_ultimate.py /directTDoA_static_server_list.db: -------------------------------------------------------------------------------- 1 | [] -------------------------------------------------------------------------------- /icon.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/llinkz/directTDoA/HEAD/icon.gif -------------------------------------------------------------------------------- /maps/directTDoA_map.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/llinkz/directTDoA/HEAD/maps/directTDoA_map.jpg -------------------------------------------------------------------------------- /maps/directTDoA_map_grayscale_with_sea.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/llinkz/directTDoA/HEAD/maps/directTDoA_map_grayscale_with_sea.jpg -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Python 2.7 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # directTDoA 7 | directTDoA_server_list.db.bak* 8 | directTDoA.cfg.bak* 9 | -------------------------------------------------------------------------------- /maps/How to find the other maps.txt: -------------------------------------------------------------------------------- 1 | Additional World maps are stored here: 2 | http://linkz.ddns.net/dmaps/ 3 | 4 | download them to /maps directory and they will be available in the "Browse map folder" -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "TDoA"] 2 | path = TDoA 3 | url = https://github.com/hcab14/TDoA 4 | ignore = dirty 5 | [submodule "kiwiclient"] 6 | path = kiwiclient 7 | url = https://github.com/jks-prv/kiwiclient 8 | ignore = dirty 9 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 7 | -------------------------------------------------------------------------------- /kiwiwavreader_patch.diff: -------------------------------------------------------------------------------- 1 | --- wavreader.py 2023-05-03 23:23:46.634226575 +0200 2 | +++ wavreader.py 2023-05-03 23:24:46.541523946 +0200 3 | @@ -8,7 +8,7 @@ 4 | class KiwiIQWavError(Exception): 5 | pass 6 | 7 | -class KiwiIQWavReader(collections.Iterator): 8 | +class KiwiIQWavReader(collections.abc.Iterator): 9 | def __init__(self, f): 10 | super(KiwiIQWavReader, self).__init__() 11 | self._frame_counter = 0 12 | -------------------------------------------------------------------------------- /.idea/directTDoA.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 11 | -------------------------------------------------------------------------------- /.idea/directTDoA4.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 11 | -------------------------------------------------------------------------------- /json_save_patch.diff: -------------------------------------------------------------------------------- 1 | --- json_save_cc.cc 2020-02-23 13:20:55.283898093 +0000 2 | +++ json_save_cc.cc 2020-02-23 13:20:30.051086971 +0000 3 | @@ -1,6 +1,6 @@ 4 | #include 5 | 6 | -#if OCTAVE_MAJOR_VERSION >= 4 and OCTAVE_MINOR_VERSION >= 4 7 | +#if OCTAVE_MAJOR_VERSION >= 5 or (OCTAVE_MAJOR_VERSION >= 4 and OCTAVE_MINOR_VERSION >= 4) 8 | # include 9 | # define OCT_STREAM_TYPE octave::stream 10 | # define OCT_REGEXP_REPLACE octave::regexp::replace 11 | -------------------------------------------------------------------------------- /Copying.wtfpl: -------------------------------------------------------------------------------- 1 | DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE 2 | Version 2, December 2004 3 | 4 | Copyright (C) 2004 Sam Hocevar 5 | 6 | Everyone is permitted to copy and distribute verbatim or modified 7 | copies of this license document, and changing it is allowed as long 8 | as the name is changed. 9 | 10 | DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE 11 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 12 | 13 | 0. You just DO WHAT THE FUCK YOU WANT TO. -------------------------------------------------------------------------------- /read_data_patch.diff: -------------------------------------------------------------------------------- 1 | --- tdoa_read_data.m 2020-02-03 21:07:32.745370216 +0000 2 | +++ tdoa_read_data2.m 2020-01-24 21:22:43.892867681 +0000 3 | @@ -83,11 +83,11 @@ 4 | b = input(i).tt1; 5 | input(i).t(b) = []; 6 | input(i).z(b) = []; 7 | - input(i).use = numel(input(i).z)/input(i).fs > 10; 8 | + input(i).use = numel(input(i).z)/input(i).fs > 3; 9 | if ~input(i).use 10 | printf('tdoa_read_data: %-40s excluded (%.2f sec < %g sec overlap)\n', ... 11 | - input(i).fn, numel(input(i).z)/input(i).fs, 10); 12 | - status.per_file(i).message = sprintf('excluded (%.2f sec < %g sec overlap)', numel(input(i).z)/12000, 10); 13 | + input(i).fn, numel(input(i).z)/input(i).fs, 3); 14 | + status.per_file(i).message = sprintf('excluded (%.2f sec < %g sec overlap)', numel(input(i).z)/12000, 3); 15 | end 16 | end 17 | 18 | -------------------------------------------------------------------------------- /kiwiclient_patch.diff: -------------------------------------------------------------------------------- 1 | --- client.py 2020-01-24 21:05:52.530626977 +0000 2 | +++ client.py 2020-02-03 20:51:42.290914515 +0000 3 | @@ -292,12 +292,12 @@ 4 | else: 5 | logging.debug("recv MSG (%s) %s: %s", self._stream_name, name, value) 6 | # Handle error conditions 7 | - if name == 'too_busy': 8 | - raise KiwiTooBusyError('%s: all %s client slots taken' % (self._options.server_host, value)) 9 | - if name == 'badp' and value == '1': 10 | - raise KiwiBadPasswordError('%s: bad password' % self._options.server_host) 11 | - if name == 'down': 12 | - raise KiwiDownError('%s: server is down atm' % self._options.server_host) 13 | + # if name == 'too_busy': 14 | + # raise KiwiTooBusyError('%s: all %s client slots taken' % (self._options.server_host, value)) 15 | + # if name == 'badp' and value == '1': 16 | + # raise KiwiBadPasswordError('%s: bad password' % self._options.server_host) 17 | + # if name == 'down': 18 | + # raise KiwiDownError('%s: server is down atm' % self._options.server_host) 19 | # Handle data items 20 | if name == 'audio_rate': 21 | self._set_ar_ok(int(value), 44100) 22 | -------------------------------------------------------------------------------- /modsocket_patch.diff: -------------------------------------------------------------------------------- 1 | --- util.py 2021-02-21 15:10:41.318806871 +0100 2 | +++ util.py 2021-02-21 15:10:39.728806882 +0100 3 | @@ -33,6 +33,7 @@ 4 | 5 | import array 6 | import errno 7 | +import sys 8 | 9 | # Import hash classes from a module available and recommended for each Python 10 | # version and re-export those symbol. Use sha and md5 module in Python 2.4, and 11 | @@ -198,7 +199,10 @@ 12 | def _mask_using_array(self, s): 13 | """Perform the mask via python.""" 14 | result = array.array('B') 15 | - result.fromstring(bytes(s)) 16 | + if sys.version_info[0] == 3 and sys.version_info[1] >= 9: 17 | + result.frombytes(bytes(s)) 18 | + else: 19 | + result.fromstring(bytes(s)) 20 | 21 | # Use temporary local variables to eliminate the cost to access 22 | # attributes 23 | @@ -214,8 +218,10 @@ 24 | masking_key_index = (masking_key_index + 1) % masking_key_size 25 | 26 | self._masking_key_index = masking_key_index 27 | - 28 | - return result.tostring() 29 | + if sys.version_info[0] == 3 and sys.version_info[1] >= 9: 30 | + return result.tobytes() 31 | + else: 32 | + return result.tostring() 33 | 34 | if 'fast_masking' in globals(): 35 | mask = _mask_using_swig 36 | -------------------------------------------------------------------------------- /nognss.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | """ This python script removes GNSS data from KiwiSDR IQ files 4 | credits: Daniel Ekmann & linkz 5 | usage ./plot_iq.py """ 6 | 7 | # python 2/3 compatibility 8 | from __future__ import print_function 9 | from __future__ import division 10 | from __future__ import absolute_import 11 | 12 | import struct 13 | import os 14 | import glob 15 | 16 | os.makedirs('NOGPS') 17 | 18 | 19 | def removegnss(source): 20 | """ Removes GNSS from the KiwiSDR original IQ file and save it as standard wav. """ 21 | old_f = open(source, 'rb') 22 | new_f = open(os.path.join('NOGPS') + os.sep + source[:-4] + '.nogps.wav', 'wb') 23 | old_size = os.path.getsize(source) 24 | data_size = 2048 * ((old_size - 36) // 2074) 25 | new_f.write(old_f.read(36)) 26 | new_f.write(b'data') 27 | new_f.write(struct.pack(' max_snr: 68 | max_snr = snr 69 | return max_snr 70 | 71 | 72 | def plot(files, order, cols): 73 | """ Plotting. """ 74 | rows = int(math.ceil(len(files) / cols)) 75 | fig, axs = plt.subplots(ncols=cols, nrows=rows) 76 | fig.set_figwidth(cols * 6.4) 77 | fig.set_figheight(rows * 4.8) 78 | for i, (path, _) in enumerate(order): 79 | a_x = axs.flat[i] 80 | a_x.specgram(files[path], NFFT=1024, Fs=12000, window=lambda data: data * np.hanning(len(data)), noverlap=512, 81 | vmin=10, vmax=200, cmap=COLORMAP) 82 | a_x.set_title(path.rsplit('_', 3)[2]) 83 | a_x.axes.get_yaxis().set_visible(False) 84 | # a_x.get_yaxis().set_major_formatter(tkr.FuncFormatter(lambda x, pos: '{0:g}'.format(x // 1e3))) 85 | # a_x.set_ybound(-6000.0, 6000.0) 86 | for i in range(len(scores), len(axs.flat)): 87 | fig.delaxes(axs.flat[i]) 88 | fig.tight_layout(rect=[0, 0.03, 1, 0.95]) 89 | for mfile in glob.glob('*.*m*'): 90 | oct_file = open(mfile, 'r') 91 | file_d = oct_file.read() 92 | oct_file.close() 93 | fig.suptitle(re.search(r".+title.+\'(.+)\'", file_d).group(1), fontsize=20) 94 | fig.savefig('TDoA_' + path.rsplit('_', 3)[1] + '_spectrogram.pdf', bbox_inches='tight', dpi=50) 95 | 96 | 97 | if __name__ == '__main__': 98 | files = {path: load(path) for path in glob.glob('*.wav*')} 99 | scores = sorted([(path, score(data)) for path, data in files.items()], key=lambda item: item[1], reverse=True) 100 | plot(files, scores, cols=3) 101 | -------------------------------------------------------------------------------- /directTDoA.cfg: -------------------------------------------------------------------------------- 1 | { 2 | "guicolors": { 3 | "main_b": "#d9d9d9", 4 | "stat_b": "#000000", 5 | "main_f": "#000000", 6 | "stat_f": "#ff0000", 7 | "cons_b": "#000000", 8 | "grad": 7, 9 | "cons_f": "#00ff00", 10 | "thres": 150 11 | }, 12 | "iq": { 13 | "pkj": "false", 14 | "uc": "false", 15 | "mode": "true" 16 | }, 17 | "tcp": { 18 | "host": "127.0.0.1", 19 | "port": 55554, 20 | "duration": 20, 21 | "word": "[NORMAL MODE]" 22 | }, 23 | "map": { 24 | "std": "#00ff00", 25 | "icontype": 1, 26 | "blk": "#ff0000", 27 | "iconsize": 2, 28 | "mapfl": 1, 29 | "hlt": "#ffffff", 30 | "fav": "#ffff00", 31 | "file": "maps/directTDoA_map.jpg", 32 | "poi": "#ff7400", 33 | "y1": "944.0", 34 | "y0": "169.0", 35 | "x0": "1495.0", 36 | "x1": "2924.0", 37 | "mapbox": "streets-v11", 38 | "gui": "1429x775+0+27" 39 | }, 40 | "nodes": { 41 | "blacklist": [], 42 | "whitelist": [ 43 | "b0d5ccfc816b", 44 | "985dad81139c", 45 | "689e198fa238", 46 | "985dad35082b", 47 | "985dad7f54e1", 48 | "884aeaf53205", 49 | "985dad7f1af5", 50 | "884aeadf9485", 51 | "985dad488ca5", 52 | "88c2556b3d70", 53 | "9059af4c1331", 54 | "985dad32f160", 55 | "985dad818d3e", 56 | "985dad7f4a82", 57 | "38d2693e706f", 58 | "985dad382ec3", 59 | "884aeaf5408e", 60 | "c4f312a68ad5", 61 | "985dad7f5e06", 62 | "38d2697cd2cd", 63 | "c4f312724e8b", 64 | "884aeaf55c98", 65 | "40bd322ecc6d", 66 | "9884e393ae55", 67 | "40bd32e4ffc8", 68 | "38d2697cd29d", 69 | "b0d5ccfdb8da", 70 | "40bd322eb47c", 71 | "c4f312755668", 72 | "985dad87b076", 73 | "b0d5cc571028", 74 | "40bd32c8d066", 75 | "40bd32e4c880", 76 | "40bd32e4d538", 77 | "c4f312a678db", 78 | "40bd32e93a3e", 79 | "9884e38cd8b8", 80 | "985dad48e79f", 81 | "907065cc0731", 82 | "c4f312b9fa95", 83 | "38d2697ce5ac", 84 | "40bd32e5123d", 85 | "c4f312724129", 86 | "985dad7f150b", 87 | "0479b71beb8a", 88 | "38d2697cc238", 89 | "985dad7f7ad6", 90 | "883f4a9a75ef", 91 | "883f4a8c7e84", 92 | "883f4aab6d9f", 93 | "38d2697cbb2b", 94 | "780473832efb", 95 | "780473505c0d", 96 | "4c3fd33a816e", 97 | "9059af54c6d8", 98 | "40bd32e94221", 99 | "40bd32e54d17", 100 | "b0d5cc571067", 101 | "985dad7efeca", 102 | "38d2697cae44", 103 | "84eb18e6e373", 104 | "38d2697cc202", 105 | "c4f312a68af3", 106 | "884aeaf593d0", 107 | "40bd32e4da2a", 108 | "883f4aabaff6", 109 | "9059af5e0500", 110 | "884aeaf52a6a", 111 | "40bd32e5554b", 112 | "38d2697ccb6d", 113 | "883f4aab980e", 114 | "884aeaf53712", 115 | "38d269837eca", 116 | "b0d5cc5711f4", 117 | "40bd32e53ceb", 118 | "40bd32c8d2a3", 119 | "38d2697cbb79", 120 | "e415f6f6fd1a", 121 | "e415f6f6d4d0", 122 | "0cb2b7d6cd80", 123 | "0035ff8f7152", 124 | "883f4a9a8729", 125 | "74e1827b88ed", 126 | "780473a94c5f", 127 | "28ec9aeff774", 128 | "40bd32e93def", 129 | "38d2697cc6e3", 130 | "985dad7f54fc", 131 | "c4f312ba0a2e", 132 | "0035ff9baf06", 133 | "3403de631d0d", 134 | "9884e3930faf", 135 | "606405357b64", 136 | "884aeaf59cc6", 137 | "0035ff8fa43d", 138 | "ffffffffffff", 139 | "985dad7f0eb7", 140 | "04a316eda0a3", 141 | "40bd322ec32b", 142 | "c4f312727699", 143 | "28ec9af0cb86", 144 | "38d2693e89d5", 145 | "1442fc102a45", 146 | "e062346fdb39", 147 | "38d269837796", 148 | "b0d5cc571db4", 149 | "28ec9ae91aec", 150 | "40bd32e51c9f", 151 | "40bd32e4d511", 152 | "e45f01029752", 153 | "04a316df1bca", 154 | "c4f312b9ff12", 155 | "883f4aa8cf37", 156 | "f045da7def63", 157 | "88c2556bd4a5", 158 | "1cba8c980f54", 159 | "6433db48e8b4", 160 | "38d2697ccb40", 161 | "985dad7f2a14", 162 | "884aeaf58c0f", 163 | "74e1825d65ae", 164 | "544a16bbe1cb", 165 | "6433db5a6a4c", 166 | "6433db1ba09c", 167 | "0035ff994d4c", 168 | "883f4a982497", 169 | "780473831fb6", 170 | "c4f312726fdf", 171 | "78047338b8a5", 172 | "9059af5c2844", 173 | "38d269837ec7", 174 | "6433db20ab39", 175 | "884aeaf5558a", 176 | "e062346fe688", 177 | "3403de9a653c", 178 | "884aeaf5858e", 179 | "6433db49bc88", 180 | "40bd32e9420f", 181 | "6433db174cbe", 182 | "3403de81254c", 183 | "40bd32c8d9ea", 184 | "b8804f25b074", 185 | "985dad4279a3", 186 | "e8eb11d2fb52", 187 | "1442fc0f98c1", 188 | "1862e4d0c3f3", 189 | "40bd322ebe12", 190 | "78047383479a", 191 | "883f4aa8d915", 192 | "c4f312818918", 193 | "985dad810cef", 194 | "780473834f23", 195 | "38d2697cc27d", 196 | "6433db38a069", 197 | "40bd32e50db4", 198 | "544a16e70e44", 199 | "985dad48d15a", 200 | "20d7787c8c5e", 201 | "1442fc0f74f7", 202 | "985dad7f80cb", 203 | "e062346fd635", 204 | "e062346fcf7b", 205 | "3484e40060d8", 206 | "40bd32c8d280", 207 | "985dad7f80a4", 208 | "0035ff9a436a", 209 | "1442fc0f664e", 210 | "985dad4533a3", 211 | "e415f6f74598", 212 | "28ec9af1610a" 213 | ] 214 | }, 215 | "presets(x0/y1/x1/y0)": { 216 | "ME": [ 217 | 25, 218 | 45, 219 | 65, 220 | 10 221 | ], 222 | "US": [ 223 | -125, 224 | 50, 225 | -66, 226 | 23 227 | ], 228 | "W": [ 229 | -179, 230 | 89, 231 | 179, 232 | -59 233 | ], 234 | "SAM": [ 235 | -85, 236 | 15, 237 | -30, 238 | -60 239 | ], 240 | "AF": [ 241 | -20, 242 | 40, 243 | 55, 244 | -35 245 | ], 246 | "SEAS": [ 247 | 85, 248 | 30, 249 | 155, 250 | -12 251 | ], 252 | "O": [ 253 | 110, 254 | -10, 255 | 180, 256 | -50 257 | ], 258 | "NAM": [ 259 | -170, 260 | 82, 261 | -50, 262 | 13 263 | ], 264 | "SAS": [ 265 | 60, 266 | 39, 267 | 100, 268 | 4 269 | ], 270 | "WR": [ 271 | 27, 272 | 72, 273 | 90, 274 | 40 275 | ], 276 | "CAM": [ 277 | -120, 278 | 33, 279 | -50, 280 | 5 281 | ], 282 | "EU": [ 283 | -30, 284 | 72, 285 | 55, 286 | 25 287 | ], 288 | "EAS": [ 289 | 73, 290 | 55, 291 | 147, 292 | 10 293 | ], 294 | "ER": [ 295 | 90, 296 | 82, 297 | 180, 298 | 40 299 | ] 300 | } 301 | } -------------------------------------------------------------------------------- /getmap.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | """ get mapbox files, draw POIs and merge to pdf. (linkz 2019) 4 | usage: ./getmap.py 5 | known bug: maximum 24 mapmarkers are allowed at this time : fixed for IQ recordings < 22 """ 6 | 7 | import sys 8 | import os 9 | import json 10 | import glob 11 | import re 12 | import threading 13 | import shutil 14 | import platform 15 | from io import BytesIO 16 | import requests 17 | import numpy as np 18 | from PIL import Image, ImageDraw, ImageFont 19 | if sys.version_info[0] == 2: 20 | import urllib 21 | else: 22 | import urllib.parse 23 | 24 | # : 25 | # streets-v11 / outdoors-v11 / light-v10 / dark-v10 26 | # satellite-v9 / satellite-streets-v11 27 | # navigation-preview-day-v4 / navigation-preview-night-v4 28 | # navigation-guidance-day-v4 / navigation-guidance-night-v4 29 | 30 | MAP_TOK = "Your_Mapbox_token_here" 31 | DATA_L = [] 32 | MAPBOX_ZOOM = {'2': [0, 0], '4': [900, 0], '6': [0, 600], '8': [900, 600]} 33 | NB_OF_NODES = len(glob.glob1(sys.argv[6].rsplit(os.sep, 1)[0], "*.wav")) 34 | NB_OF_FILES = 0 35 | if platform.system() == "Windows": 36 | FONT = ImageFont.truetype(r'C:\Windows\Fonts\arial.ttf', 18) 37 | else: 38 | FONT = ImageFont.truetype('Pillow/Tests/fonts/DejaVuSans.ttf', 18) 39 | NEW_IMAGE = Image.new("RGB", (1800, 1200)) 40 | 41 | for wavfiles in glob.glob(sys.argv[6].rsplit(os.sep, 1)[0] + os.sep + "*.wav"): 42 | for gnssfiles in glob.glob("gnss_pos/*.txt"): 43 | if wavfiles.rsplit("_", 2)[1] + ".txt" == gnssfiles.rsplit(os.sep, 2)[1]: 44 | DATA_LATLON = {} 45 | filent2 = open("gnss_pos/" + gnssfiles.rsplit(os.sep, 2)[1], 'r') 46 | data2 = filent2.readlines() 47 | DATA_LATLON['lon'] = data2[0].rsplit("[", 1)[1].rsplit("]", 1)[0].rsplit(",", 1)[1] 48 | DATA_LATLON['lat'] = data2[0].rsplit("[", 1)[1].rsplit("]", 1)[0].rsplit(",", 1)[0] 49 | DATA_LATLON['size'] = "small" 50 | DATA_LATLON['symbol'] = "triangle" 51 | filent = open(os.getcwd() + os.sep + sys.argv[7], 'r') 52 | if wavfiles.rsplit("_", 2)[1] in filent.read(): 53 | DATA_LATLON['color'] = "#ff0" 54 | DATA_L.append(DATA_LATLON) 55 | elif NB_OF_NODES <= 22: 56 | DATA_LATLON['color'] = "#999" 57 | DATA_L.append(DATA_LATLON) 58 | 59 | # TDoA coordinates and style 60 | DATA_LATLON = dict() 61 | DATA_LATLON['lon'] = sys.argv[2] 62 | DATA_LATLON['lat'] = sys.argv[1] 63 | DATA_LATLON['size'] = "medium" 64 | DATA_LATLON['color'] = "#d00" 65 | DATA_LATLON['symbol'] = "" 66 | DATA_L.append(DATA_LATLON) 67 | 68 | # Known point coordinates and style (if filled) 69 | if sys.argv[3] != "0" and sys.argv[4] != "0": 70 | DATA_LATLON = dict() 71 | DATA_LATLON['lon'] = sys.argv[4] 72 | DATA_LATLON['lat'] = sys.argv[3] 73 | DATA_LATLON['size'] = "small" 74 | DATA_LATLON['color'] = "#f70" 75 | DATA_LATLON['symbol'] = "star" 76 | DATA_L.append(DATA_LATLON) 77 | 78 | # Mapbox's geojson maker 79 | GEOJSON = { 80 | "type": "FeatureCollection", 81 | "features": [ 82 | { 83 | "type": "Feature", 84 | "geometry": { 85 | "type": "Point", 86 | "coordinates": [d["lon"], d["lat"]], 87 | }, 88 | "properties": { 89 | "marker-size": d["size"], 90 | "marker-color": d["color"], 91 | "marker-symbol": d["symbol"] 92 | }, 93 | } for d in DATA_L] 94 | } 95 | 96 | # Remove double-quotes on coordinates and convert json to url-style 97 | GEOJSON = re.sub(r'\"(-?\d+(\.\d+)?)\"', r'\1', json.dumps(GEOJSON)) 98 | if sys.version_info[0] == 2: 99 | GEOJSON = urllib.quote(GEOJSON) 100 | else: 101 | GEOJSON = urllib.parse.quote(GEOJSON) 102 | 103 | 104 | def haversine_distance(lat1, lon1, lat2, lon2): 105 | """ code source from Dario Radečić https://medium.com/@radecicdario 106 | https://towardsdatascience.com/heres-how-to-calculate-distance-between-2-geolocations-in-python-93ecab5bbba4 """ 107 | r = 6371 108 | phi1 = np.radians(lat1) 109 | phi2 = np.radians(lat2) 110 | delta_phi = np.radians(lat2 - lat1) 111 | delta_lambda = np.radians(lon2 - lon1) 112 | a = np.sin(delta_phi / 2)**2 + np.cos(phi1) * np.cos(phi2) * np.sin(delta_lambda / 2)**2 113 | res = r * (2 * np.arctan2(np.sqrt(a), np.sqrt(1 - a))) 114 | return np.round(res, 2) 115 | 116 | 117 | class GetMaps(threading.Thread): 118 | """ Curl processing routine """ 119 | 120 | def __init__(self, zooming=None, x=None, y=None): 121 | super(GetMaps, self).__init__() 122 | self.zoom2 = zooming 123 | self.x_pos = x 124 | self.y_pos = y 125 | 126 | def run(self): 127 | global NB_OF_FILES 128 | req = requests.get( 129 | "https://api.mapbox.com/styles/v1/mapbox/" + sys.argv[5] + "/static/geojson(" + GEOJSON + ')/' + sys.argv[ 130 | 2] + ',' + sys.argv[1] + "," + self.zoom2 + "/900x600?access_token=" + MAP_TOK, timeout=2, 131 | stream=True) 132 | if req.status_code == 200: 133 | new_pic = BytesIO() 134 | req.raw.decode_content = True 135 | shutil.copyfileobj(req.raw, new_pic) 136 | img = Image.open(new_pic) 137 | NEW_IMAGE.paste(img, (self.x_pos, self.y_pos)) 138 | NB_OF_FILES += 1 139 | if NB_OF_FILES == 4: 140 | draw = ImageDraw.Draw(NEW_IMAGE) 141 | text_length = 10.5 * len(sys.argv[1] + " " + sys.argv[2]) 142 | draw.polygon([(395, 255), (395, 235), (395 + text_length, 235), (395 + text_length, 255)], fill="#222") 143 | draw.polygon([(1295, 255), (1295, 235), (1295 + text_length, 235), (1295 + text_length, 255)], 144 | fill="#222") 145 | draw.polygon([(395, 855), (395, 835), (395 + text_length, 835), (395 + text_length, 855)], fill="#222") 146 | draw.polygon([(1295, 855), (1295, 835), (1295 + text_length, 835), (1295 + text_length, 855)], 147 | fill="#222") 148 | [(draw.text((x, y), sys.argv[1] + " " + sys.argv[2], fill="#fff", font=FONT)) for x in [400, 1300] for y 149 | in [235, 835]] 150 | # distance between most likely & known point red rectangle label 151 | if sys.argv[3] != "-90" and sys.argv[4] != "180": 152 | distance = "Distance between TDoA most likely location and map marker = " + str( 153 | haversine_distance(float(sys.argv[1]), float(sys.argv[2]), float(sys.argv[3]), 154 | float(sys.argv[4]))) + " km" 155 | draw.polygon([(0, 0), (900, 0), (900, 20), (0, 20)], fill="#FFF") 156 | draw.text((0, 0), distance, fill="#000", font=FONT) 157 | draw.line((900, 0, 900, 1200), fill="#000") 158 | draw.line((0, 600, 1800, 600), fill="#000") 159 | NEW_IMAGE.save(sys.argv[6].replace("_[[]", "_[").replace("[]]_", "]_").replace(".png", ".pdf"), "PDF", 160 | resolution=144, append_images=[NEW_IMAGE]) 161 | else: 162 | img = Image.new('RGB', (900, 600), (255, 255, 255)) 163 | NEW_IMAGE.paste(img, (self.x_pos, self.y_pos)) 164 | NB_OF_FILES += 1 165 | if NB_OF_FILES == 4: 166 | draw = ImageDraw.Draw(NEW_IMAGE) 167 | draw.text((15, 15), "Error: unable to get maps, check Mapbox token", (0, 0, 0), font=FONT) 168 | NEW_IMAGE.save(sys.argv[6].replace("_[[]", "_[").replace("[]]_", "]_").replace(".png", ".pdf"), "PDF", 169 | resolution=144, append_images=[NEW_IMAGE]) 170 | 171 | 172 | if sys.version_info[0] == 2: 173 | for zoom, placement in MAPBOX_ZOOM.iteritems(): 174 | GetMaps(zoom, placement[0], placement[1]).start() 175 | else: 176 | for zoom, placement in MAPBOX_ZOOM.items(): 177 | GetMaps(zoom, placement[0], placement[1]).start() 178 | -------------------------------------------------------------------------------- /kiwirecorder_patch.diff: -------------------------------------------------------------------------------- 1 | --- kiwirecorder.py 2024-01-14 09:48:45.338711534 +0100 2 | +++ kiwirecorder.py 2024-01-14 09:36:05.647684241 +0100 3 | @@ -10,6 +10,10 @@ 4 | from kiwi import KiwiSDRStream, KiwiWorker 5 | from optparse import OptionParser 6 | from optparse import OptionGroup 7 | +import sounddevice 8 | + 9 | +stream = sounddevice.OutputStream(48000, 2048, channels=1, dtype='int16') 10 | +stream.start() 11 | 12 | HAS_RESAMPLER = True 13 | try: 14 | @@ -21,11 +25,12 @@ 15 | 16 | 17 | def _write_wav_header(fp, filesize, samplerate, num_channels, is_kiwi_wav): 18 | + samplerate = int(samplerate+0.5); 19 | fp.write(struct.pack('<4sI4s', b'RIFF', filesize - 8, b'WAVE')) 20 | bits_per_sample = 16 21 | byte_rate = samplerate * num_channels * bits_per_sample // 8 22 | block_align = num_channels * bits_per_sample // 8 23 | - fp.write(struct.pack('<4sIHHIIHH', b'fmt ', 16, 1, num_channels, int(samplerate+0.5), byte_rate, block_align, bits_per_sample)) 24 | + fp.write(struct.pack('<4sIHHIIHH', b'fmt ', 16, 1, num_channels, samplerate, byte_rate, block_align, bits_per_sample)) 25 | if not is_kiwi_wav: 26 | fp.write(struct.pack('<4sI', b'data', filesize - 12 - 8 - 16 - 8)) 27 | 28 | @@ -151,8 +156,10 @@ 29 | # For AM, ignore the low pass filter cutoff 30 | lp_cut = -hp_cut if hp_cut is not None else hp_cut 31 | self.set_mod(mod, lp_cut, hp_cut, self._freq) 32 | - if self._options.agc_gain != None: 33 | - self.set_agc(on=False, gain=self._options.agc_gain) 34 | + if self._options.agc_gain is not None: 35 | + self.set_agc(on=False, gain=self._options.agc_gain[0], hang=self._options.agc_gain[1], 36 | + thresh=self._options.agc_gain[2], slope=self._options.agc_gain[3], 37 | + decay=self._options.agc_gain[4]) 38 | else: 39 | self.set_agc(on=True) 40 | if self._options.compression is False: 41 | @@ -267,34 +274,37 @@ 42 | 43 | # fp.tell() sometimes returns zero. _write_wav_header writes filesize - 8 44 | if filesize >= 8: 45 | - _write_wav_header(fp, filesize, int(self._output_sample_rate), self._num_channels, self._options.is_kiwi_wav) 46 | + _write_wav_header(fp, filesize, self._output_sample_rate, self._num_channels, self._options.is_kiwi_wav) 47 | 48 | def _write_samples(self, samples, *args): 49 | - """Output to a file on the disk.""" 50 | - now = time.gmtime() 51 | - sec_of_day = lambda x: 3600*x.tm_hour + 60*x.tm_min + x.tm_sec 52 | - dt_reached = self._options.dt != 0 and self._start_ts is not None and sec_of_day(now)//self._options.dt != sec_of_day(self._start_ts)//self._options.dt 53 | - if self._start_ts is None or (self._options.filename == '' and dt_reached): 54 | - self._start_ts = now 55 | - self._start_time = time.time() 56 | - # Write a static WAV header 57 | - with open(self._get_output_filename(), 'wb') as fp: 58 | - _write_wav_header(fp, 100, int(self._output_sample_rate), self._num_channels, self._options.is_kiwi_wav) 59 | - if self._options.is_kiwi_tdoa: 60 | - # NB: MUST be a print (i.e. not a logging.info) 61 | - print("file=%d %s" % (self._options.idx, self._get_output_filename())) 62 | - else: 63 | - logging.info("Started a new file: %s" % self._get_output_filename()) 64 | - with open(self._get_output_filename(), 'ab') as fp: 65 | - if self._options.is_kiwi_wav: 66 | - gps = args[0] 67 | - self._gnss_performance.analyze(self._get_output_filename(), gps) 68 | - fp.write(struct.pack('<4sIBBII', b'kiwi', 10, gps['last_gps_solution'], 0, gps['gpssec'], gps['gpsnsec'])) 69 | - sample_size = samples.itemsize * len(samples) 70 | - fp.write(struct.pack('<4sI', b'data', sample_size)) 71 | - # TODO: something better than that 72 | - samples.tofile(fp) 73 | - self._update_wav_header() 74 | + """Output to a file on the disk OR give me sound ! """ 75 | + if self._options.audio: 76 | + stream.write(np.array(samples, dtype=np.int16)) 77 | + else: 78 | + now = time.gmtime() 79 | + sec_of_day = lambda x: 3600*x.tm_hour + 60*x.tm_min + x.tm_sec 80 | + dt_reached = self._options.dt != 0 and self._start_ts is not None and sec_of_day(now)//self._options.dt != sec_of_day(self._start_ts)//self._options.dt 81 | + if self._start_ts is None or (self._options.filename == '' and dt_reached): 82 | + self._start_ts = now 83 | + self._start_time = time.time() 84 | + # Write a static WAV header 85 | + with open(self._get_output_filename(), 'wb') as fp: 86 | + _write_wav_header(fp, 100, self._output_sample_rate, self._num_channels, self._options.is_kiwi_wav) 87 | + if self._options.is_kiwi_tdoa: 88 | + # NB: MUST be a print (i.e. not a logging.info) 89 | + print("file=%d %s" % (self._options.idx, self._get_output_filename())) 90 | + else: 91 | + logging.info("Started a new file: %s" % self._get_output_filename()) 92 | + with open(self._get_output_filename(), 'ab') as fp: 93 | + if self._options.is_kiwi_wav: 94 | + gps = args[0] 95 | + self._gnss_performance.analyze(self._get_output_filename(), gps) 96 | + fp.write(struct.pack('<4sIBBII', b'kiwi', 10, gps['last_gps_solution'], 0, gps['gpssec'], gps['gpsnsec'])) 97 | + sample_size = samples.itemsize * len(samples) 98 | + fp.write(struct.pack('<4sI', b'data', sample_size)) 99 | + # TODO: something better than that 100 | + samples.tofile(fp) 101 | + self._update_wav_header() 102 | 103 | def _on_gnss_position(self, pos): 104 | pos_record = False 105 | @@ -371,9 +381,9 @@ 106 | opt_single.server_host = s 107 | opt_single.status = 0 108 | 109 | - # time() returns seconds, so add pid and host index to make tstamp unique per connection 110 | + # time() returns seconds, so add pid and host index to make timestamp unique per connection 111 | opt_single.timestamp = int(time.time() + os.getpid() + i) & 0xffffffff 112 | - for x in ['server_port', 'password', 'tlimit_password', 'frequency', 'agc_gain', 'filename', 'station', 'user']: 113 | + for x in ['server_port', 'password', 'tlimit_password', 'frequency', 'filename', 'station', 'user']: 114 | opt_single.__dict__[x] = _sel_entry(i, opt_single.__dict__[x]) 115 | l.append(opt_single) 116 | multiple_connections = i 117 | @@ -567,6 +577,11 @@ 118 | default=False, 119 | action='store_true', 120 | help='Also process sound data when in waterfall or S-meter mode (sound connection options above apply)') 121 | + group.add_option('-a', '--audio', 122 | + dest='audio', 123 | + default=False, 124 | + action='store_true', 125 | + help='Get audio output instead of writing to disk (mod by linkz)') 126 | parser.add_option_group(group) 127 | 128 | group = OptionGroup(parser, "S-meter mode options", "") 129 | @@ -635,7 +650,7 @@ 130 | if opt.launch_delay != 0 and i != 0 and options[i-1].server_host == options[i].server_host: 131 | time.sleep(opt.launch_delay) 132 | r.start() 133 | - #logging.info("started sound recorder %d, tstamp=%d" % (i, options[i].timestamp)) 134 | + #logging.info("started sound recorder %d, timestamp=%d" % (i, options[i].timestamp)) 135 | logging.info("started sound recorder %d" % i) 136 | 137 | for i,r in enumerate(wf_recorders): 138 | @@ -664,6 +679,7 @@ 139 | 140 | logging.debug('gc %s' % gc.garbage) 141 | 142 | + 143 | if __name__ == '__main__': 144 | #import faulthandler 145 | #faulthandler.enable() 146 | -------------------------------------------------------------------------------- /trim_iq.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | """ credits: Daniel Ekmann for core code & linkz for GUI code 4 | usage ./trim_iq.py """ 5 | 6 | # python 2/3 compatibility 7 | from __future__ import print_function 8 | from __future__ import division 9 | from __future__ import absolute_import 10 | 11 | import subprocess 12 | import struct 13 | import os 14 | import glob 15 | import shutil 16 | import platform 17 | import sys 18 | import argparse 19 | import io 20 | from io import BytesIO 21 | import numpy as np 22 | import matplotlib.pyplot as plt 23 | from matplotlib.colors import LinearSegmentedColormap 24 | from matplotlib.widgets import SpanSelector 25 | from matplotlib.ticker import FuncFormatter 26 | 27 | TRIM_PROC = 1 28 | FILE_NUMBER = 0 29 | TOTAL_FILES = len(glob.glob("*.wav")) 30 | START = 0 31 | FREQUENCY = float(os.getcwd().rsplit("F", 1)[1]) 32 | GRADIENT = { 33 | 'red': ((0.0, 0.0, 0.0), 34 | (0.077, 0.0, 0.0), 35 | (0.16, 0.0, 0.0), 36 | (0.265, 1.0, 1.0), 37 | (0.403, 1.0, 1.0), 38 | (0.604, 1.0, 1.0), 39 | (1.0, 1.0, 1.0)), 40 | 41 | 'green': ((0.0, 0.0, 0.0), 42 | (0.077, 0.0, 0.0), 43 | (0.16, 1.0, 1.0), 44 | (0.265, 1.0, 1.0), 45 | (0.403, 0.0, 0.0), 46 | (0.604, 0.0, 0.0), 47 | (1.0, 0.764, 0.764)), 48 | 49 | 'blue': ((0.0, 0.117, 0.117), 50 | (0.077, 1.0, 1.0), 51 | (0.16, 1.0, 1.0), 52 | (0.265, 0.0, 0.0), 53 | (0.403, 0.0, 0.0), 54 | (0.604, 1.0, 1.0), 55 | (1.0, 1.0, 1.0)), 56 | } 57 | COLORMAP = LinearSegmentedColormap('SAColorMap', GRADIENT, 1024) 58 | 59 | 60 | def on_press(event): 61 | """ Close IQ preview plot with mouse click. """ 62 | 63 | plt.close() 64 | 65 | 66 | def on_mouse_move(event): 67 | """ Range selection length measurement. """ 68 | 69 | if None not in (event.xdata, event.ydata) and START != 0: 70 | if -2 <= round(event.xdata - START, 2) <= 2: 71 | span.set_visible(True) 72 | span2.set_visible(False) 73 | else: 74 | span.set_visible(False) 75 | span2.set_visible(True) 76 | 77 | 78 | def on_mouse_click(event): 79 | """ First click on the spectrogram. """ 80 | 81 | global START 82 | START = event.xdata 83 | 84 | 85 | def onselect(event1, event2): 86 | """ Drag-select procedure on temp IQ stored in memory. """ 87 | 88 | global START 89 | if float(event2 - event1) <= 2: 90 | print ("Period range is too small (less than 2 sec) - deleting " + IQfile) 91 | plt.close() 92 | os.remove(IQfile) 93 | START = 0 94 | 95 | 96 | def onselect2(event10, event20): 97 | """ Drag-select procedure on temp IQ stored in memory. """ 98 | 99 | global START 100 | if float(event20 - event10) > 2: 101 | trim_iq(round(event10, 2), round(event20, 2)) 102 | START = 0 103 | 104 | 105 | def trim_iq(from_block, to_block): 106 | """ Trim the original IQ file and save it. """ 107 | 108 | global TRIM_PROC 109 | plt.close() 110 | block_duration = 512 / 12000.0 111 | from_idx = int(float(from_block) // block_duration) 112 | to_idx = int(float(to_block) // block_duration) 113 | old_f = open(IQfile, 'rb') 114 | new_f = io.BytesIO() 115 | new_f.write(b'RIFF') 116 | new_f.write(struct.pack('0: 102 | - span = full_span / 2.**zoom 103 | +offset_khz = options['start'] # this is offset in kHz 104 | +full_span = 30000 # for a 30MHz kiwiSDR 105 | +if zoom > 0: 106 | + span = full_span // 2.**zoom 107 | else: 108 | - span = full_span 109 | - 110 | -rbw = span/bins 111 | -if offset_khz>0: 112 | -# offset = (offset_khz-span/2)/(full_span/bins)*2**(zoom)*1000. 113 | - offset = (offset_khz+100)/(full_span/bins)*2**(4)*1000. 114 | - offset = max(0, offset) 115 | + span = full_span 116 | +rbw = span//bins 117 | +if offset_khz > 0: 118 | + offset = (offset_khz+100)//(full_span//bins)*2**4*1000. 119 | + offset = max(0, offset) 120 | else: 121 | - offset = 0 122 | - 123 | -print span, offset 124 | - 125 | -center_freq = span/2+offset_khz 126 | -print "Center frequency: %.3f MHz" % (center_freq/1000) 127 | - 128 | -now = str(datetime.now()) 129 | + offset = 0 130 | +center_freq = span//2+offset_khz 131 | +now = b'(datetime.now()' 132 | header = [center_freq, span, now] 133 | header_bin = struct.pack("II26s", *header) 134 | 135 | -print "Trying to contact server..." 136 | try: 137 | mysocket = socket.socket() 138 | mysocket.connect((host, port)) 139 | except: 140 | - print "Failed to connect" 141 | - exit() 142 | -print "Socket open..." 143 | + print("Failed to connect") 144 | + exit() 145 | 146 | uri = '/%d/%s' % (int(time.time()), 'W/F') 147 | handshake = wsclient.ClientHandshakeProcessor(mysocket, host, port) 148 | handshake.handshake(uri) 149 | - 150 | request = wsclient.ClientRequest(mysocket) 151 | request.ws_version = mod_pywebsocket.common.VERSION_HYBI13 152 | - 153 | stream_option = StreamOptions() 154 | stream_option.mask_send = True 155 | stream_option.unmask_receive = False 156 | - 157 | mystream = Stream(request, stream_option) 158 | -print "Data stream active..." 159 | - 160 | 161 | # send a sequence of messages to the server, hardcoded for now 162 | # max wf speed, no compression 163 | -msg_list = ['SET auth t=kiwi p=', 'SET zoom=%d start=%d'%(zoom,offset),\ 164 | -'SET maxdb=0 mindb=-100', 'SET wf_speed=4', 'SET wf_comp=0'] 165 | +msg_list = ['SET auth t=kiwi p=', 'SET zoom=%d start=%d' % (zoom, offset), 166 | + 'SET maxdb=0 mindb=-100', 'SET wf_speed=4', 'SET wf_comp=0'] 167 | for msg in msg_list: 168 | mystream.send_message(msg) 169 | -print "Starting to retrieve waterfall data..." 170 | # number of samples to draw from server 171 | length = options['length'] 172 | # create a numpy array to contain the waterfall data 173 | wf_data = np.zeros((length, bins)) 174 | binary_wf_list = [] 175 | time = 0 176 | -while time### 03 Jan 2024 - KiwiSDR software version > v1.647 broke directTDoA (no GNSS/GPS detected because of IQ wav incompatibility) 4 | >### 12 Jan 2024 - Good news! Christoph, our TDoA master fixed the issue (was a bug in kiwirecorder.py) 5 | >#### FIX : https://github.com/jks-prv/kiwiclient/commit/bc189087cf503820d56ddcc0f8781d7eed1b6337 6 | >#### The fix is simple and can be done by hand, just edit your kiwiclient/kiwirecorder.py file if you want a quick one ! 7 | --- 8 | ![directTDoA picture](http://linkz.ddns.net/directTDoA.png) 9 | 10 | This software is JUST a python 2/3 GUI designed to compute TDoA runs on shortwave radio transmissions using remote (GPS enabled) KiwiSDR receivers around the World. 11 | 12 | > TDoA = Time Difference of Arrival .. (in this case: the Arrival of shortwave radio transmissions) 13 | 14 | >## Linux users : GNU Octave version < 8 only ! 15 | > else, read_kiwi_iq_wav.cc will not compile - fix in progress... 16 | 17 | ## KNOWN ISSUES: 18 | 19 | #### 1/ If you plan to use the software on a machine without a sound card then you must comment out lines 15 & 16 in `/directTDoA/kiwiclient/kiwirecorder.py` 20 | `#stream = sounddevice.OutputStream(48000, 2048, channels=1, dtype='int16')` 21 | 22 | `#stream.start()` 23 | 24 | #### 2/ On recent versions of Octave the handling of the font size has been changed (pixels Vs points) and you may find that they are too large in the final file, you can reduce the fontsize values on lines 41, 42 & 154 in `/directTDoA/TDoA/m/tdoa_plot_map.m` 25 | 26 | --- 27 | ## WINDOWS 28 | 29 | ##### The decision was made not to support installation from the repository. 30 | 31 | 1/ Download the latest [directTDoA-windows.zip](https://github.com/llinkz/directTDoA/releases), unzip and extract it 32 | 33 | 2/ Create your own [Mapbox.com](https://account.mapbox.com/auth/signup/) account, go to account and get/create a default public token then edit [getmap.py](https://github.com/llinkz/directTDoA/blob/master/getmap.py#L30) and modify `MAP_TOK` variable with your default public token 34 | > NOTE: directTDoA use [Static Images API](https://docs.mapbox.com/api/maps/static-images/) and you'll get 50000 free monthly requests 35 | 36 | 3/ double-click on `directTDoA.bat` 37 | 38 | >#### IMPORTANT: You must use only this method to launch the program to avoid file path issues. 39 | #### This .zip archive contains all the necessary files already patched and compiled and also includes light versions of GNU Octave and python, so no need to install the full versions of the last two on your machine. The unzipped archive is 272 MB, compared to ~2 GB in the other installer way. 40 | 41 | --- 42 | ## LINUX 43 | 44 | 1/ Install python 3 and python3-pip using your package manager 45 | 46 | 2/ Install GNU octave (important: only versions < 8) 47 | 48 | 3/ Install git, patch, gcc, base-devel, ttf-dejavu, gcc-fortran, tk, portaudio, xdg-utils, epdfview, fltk, liboctave-dev 49 | 50 | 4/ `git clone --recursive https://github.com/llinkz/directTDoA && cd directTDoA` 51 | 52 | 5/ `./setup.sh` 53 | > ####This setup script will install python modules, compile the necessary .oct file and apply some files patchs 54 | > ####IMPORTANT: The octave files compilation process takes a lot of time, be patient, ignore warnings and don't stop the script 55 | 56 | 6/ Create your own [Mapbox.com](https://account.mapbox.com/auth/signup/) account, go to account and get/create a default public token then edit file named [getmap.py](https://github.com/llinkz/directTDoA/blob/master/getmap.py#L30) and modify `MAP_TOK` variable with your default public token (directTDoA use [Static Images API](https://docs.mapbox.com/api/maps/static-images/) and you'll get 50000 free monthly requests) 57 | 58 | 7/ `./directTDoA.py` 59 | 60 | --- 61 | ## MAC OS X 62 | 63 | * REQUIREMENT Xcode + Homebrew (https://brew.sh/index_fr) 64 | 65 | 1/ Install Homebrew, in terminal : `/usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"` 66 | 67 | 2/ Install Python, in terminal : `brew install python@2` or `brew install python@3` 68 | 69 | 3/ Install GNU Octave in Terminal : `brew install octave` 70 | 71 | 4/ `git clone --recursive https://github.com/llinkz/directTDoA && cd directTDoA` 72 | 73 | 5/ `./setup.sh` 74 | > ####This setup script will install python modules, compile the necessary .oct file and apply some files patchs 75 | > ####IMPORTANT: The octave files compilation process takes a lot of time, be patient, ignore warnings and don't stop the script 76 | 77 | 6/ Create your own [Mapbox.com](https://account.mapbox.com/auth/signup/) account, go to account and get/create a default public token then edit file named [getmap.py](https://github.com/llinkz/directTDoA/blob/master/getmap.py#L30) and modify `MAP_TOK` variable with your default public token (directTDoA use [Static Images API](https://docs.mapbox.com/api/maps/static-images/) and you'll get 50000 free monthly requests) 78 | 79 | 7/ `./directTDoA.py` 80 | 81 | --- 82 | ## LICENSE 83 | * This python GUI code has been written and released under the "do what the f$ck you want with it" license 84 | --- 85 | ## CHANGE LOG 86 | * v1.00-1.50 : first working version, basic, static map, manual host adding, hardcoded coordinates, manual octave code run etc... 87 | * v2.00 : current work, update & dynamic maps full of GPS enabled nodes, auto octave code run, easier to use 88 | * v2.10beta : adding differents maps that can be choosed by the user, early work on SNR and tiny waterfall for nodes 89 | * v2.20: adding favorite/blacklist node management, popup menu when clicking a node gives: add for TDoA proc + Open KiwiSDR in browser 90 | * v2.21: reducing the map boundaries red rectangle 'sensivity' when mouse is near main window borders 91 | * v2.30: adding node color change possible + code clean-up + adding a popup window telling you forgot to choose your map boundaries before starting the IQ recording + Popup menus are now disabled (Add/Open) if the node has no slot available + Added a gray scale map more brighter 92 | * v2.31: bugfix on checkfilesize process 93 | * v2.32: adding a restart GUI button 94 | * v2.33: adding MacOS X compatibility, thx Nicolas M. 95 | * v2.40: known points (world capitals) listing is now a file, format is 'name,lat,lon' - easier for you to add yours :-) 96 | * v2.41: update process modified due to missing tags for some nodes in kiwisdr.com/public page 97 | * v2.42: forgot some conditions for MacOS compatibility oops thanks Nicolas M. again :-) 98 | * v2.43: auto create the directTDoA_server_list.db file at 1st start, file does not need to be in the repo anymore 99 | * v2.44: MacOS tested OK, code cleanup + warning about missing GPS timestamps in IQ recordings -uglymaps +kickass NASA maps 100 | * v2.50: some TODO list items coded or fixed 101 | * v2.60: map update now based on John's json listing + GPS fix/min map filter + nodes are identified by IDs, no hosts anymore + no .png file creation (patch) + no more gnss_pos.txt backup and no more TDoA/gnss_pos/ purge 102 | * v2.70: Octave subprocess management modified (no more octave defunct remaining in "ps aux" now) + stdout & stderr saved in the same "TDoA/iq//TDoA_.txt" file 103 | * v2.71: each node color brightness is now based on its latest GPS fix/min value, it will become darker when fix/min will go towards "0" + my own kiwiSDR coordinates more accurate 104 | * v2.72: Adding the SNR values of each node from linkfanel's (JSON) database + Color points (nodes) change in brightness according to the SNR, minimum=0 maximum=35? (version not released) 105 | * v2.80: Listing update is now made from both linkfanel's (JSON) databases only (GPS enabled nodes list + SNR values) + adding regexp to create TDoA_id (parsing callsigns), IPs and node various coordinates format (version not released) 106 | * v2.90: Code clean-up + SNR values are now only from IS0KYB (JSON) database + count TDoA runs at start + adding a ./recompute.sh script to backup dir + directTDoA node db now in JSON format + add map legend + popup menu font color managed for more readibility + new maps 107 | * v3.00: More code clean-up and as the GUI has changed a lot recently, it's now entering the v.3xx version range 108 | * v3.10: Removed "20 kHz wide audio bandwidth mode" set KiwiSDRs from the node list, incompatible with TDoA at this time (2jan19) + reachability & GPS_good, fixes_min, user, users_max values are now dynamic when node is clicked on map (timeout/host not found/obsolete proxy data) 109 | * v3.20: Better management of clicked nodes (checking offline=yes/no + TDoA_ch>=1 + fixes_min>0) + default IQ rec BW in config file added + possibility to restart the IQ rec process + Marco/Pierre websites checked before update process start + current release version check menu added 110 | * v3.21: the popup when map boundaries are set has been removed - adding mode informations in the TDoA map result title - minor bug fixes with the bandwidth default/current setting 111 | * v3.22: map boundaries informations back, as label.. 112 | * v3.23: bug fixes with add/remove fav/black process.. 113 | * v3.24: allowing the possibility to "Open" a node in browser even if 0 GPS fixes were reported at instant T + minor date modification on TDoA output file title + minor text corrections 114 | * v4.00: no more GUI restart after TDoA runs (node list is kept intact) + Listen/Demod mode added, **requires python modules _pygame_ (for all) + _scipy_ (for MacOS X), new file _KiwiSDRclient.py_ also required** + possibility to remove a single node from the list + purge button added + check version runned on software start + minor fixes on many routines 115 | * v4.10: "Restart Rec" is now "Stop Rec" instead (it saves IQ files and generate .m file only) + added "Abort TDoA" routine so you can stop a previewed bad result octave process w/o having to restart full GUI + minor mods on checkversion(), float(frequency) and restart/close GUI + 200Hz high pass filter block commented out and empty known point block added in proc.m files 116 | * v4.18: early ultimateTDoA mode dev, adding a necessary patch for TDoA/kiwiclient/kiwiworker.py to bypass returned errors causing full IQ recording process freezes (KiwiBadPasswordError & KiwiDownError) **to apply run: _patch -i kiwiworker_patch.diff ./TDoA/kiwiclient/kiwiworker.py_** 117 | * v4.19: adding another patch for TDoA/m/tdoa_plot_map.m to display the 'most likely position' string in the final pdf title + exchanging lon and lat values position for better reading - **Note: that patch already contains the nopng patch previously released** 118 | * v4.20: introducing new **ultimateTDoA** mode (massive IQ recordings without octave run from the GUI), nodes selection using the same way as defining TDoA map boundaries, all IQ files saved and compute_ultimate.sh dynamic bash script created in same ./TDoA/iq/subdirectory + recomputed pdf files now containing a timestamp so you'll keep all of them (instead of overwriting the only ./TDoA/pdf one) 119 | * v5.00: big code optimization + adding waterfall/SNR measurement + removing Marco website source + keyboard shortcuts + filter/color/icon/add rem fav/blk changes w/o restart - better precision on map (coordinates with decimals) + highlight on selected nodes + plot_iq.py script (plotting IQ spectrograms w/o GPS ticks) + GUI colors management 120 | + mapbox.com world maps in final results + trim_iq.py script to modify the recorded IQ files + Sorcerer TCP client for ALE auto-detect & auto-compute TDoAs 121 | * v5.10: removed KiwiSDR nodes "names" from .db files + compute_ultimate script has been transformed into GUI with plot_iq now only displaying the selected nodes + adding command arguments to trim_iq.py script (./trim_iq.py -h ,for help) 122 | * v5.20: "directTDoA_v5.xx" now displayed on the KiwiSDR target's userlist when connected + simplification of the node rec-list management + adding a checkbox to automatically start (or not) _compute_ultimate.py_ script when "Stop Rec" button is clicked + extra command on KiwiSDR first line popup to add the node even if it has _fixes_min=0_ 123 | + new _has_gps_ routine in both _plot_iq.py_ & _compute_ultimate.py_ + bug fix: regexp wrongly detecting LON + bug fix: if Sorcerer TCP client checkbox is unchecked while recording, no more endless record session. 124 | * v6.00: Listen mode (AM/LSB/USB) is back, to get it working you must apply a patch: `patch -i kiwirecorder_patch.diff ./kiwiclient/kiwirecorder.py` from directTDoA dir and install sounddevice + samplerate python modules with `python -m pip install sounddevice samplerate` + some python 2 Vs. python 3 bug fixes + bug fixed on map update process + .desktop files creation removed 125 | * v6.10: Restart GUI routine modified + less CMD windows for Windows OS users (using pythonw instead of python) + bug fix that caused the map to move suddenly far away when selecting a node (problem only noticed on Windows OS) + no more auto-PlotIQ() start on ultimateTDoA runs + modifications of the .bat files for Windows OS users (CPU affinity of the python processes now set towards a single one, the allocation on several generated a delay in the starting of the IQ records) 126 | * v6.20: User demand and personal use: added functionality to manage the overlapping of icons on the map. Now when you click near a cluster of multiple nodes, a menu will appear and allow you to choose the one you really want + some GUI design changes + bug fix on map locations search (avoid multiple displayed) 127 | * v7.00: Adding USB/LSB/CW/AM/2kHz/4kHz/6kHz/8kHz IQ BW presets + display bug fix for known places on map + adding recorded nodes Vs selected nodes counter + recording time in file size window + IQ rec length max limit set to 120 seconds (just in case) + TCP client modified, possibility to change the trigger word (regexp supported) + PDF title mods + remember GUI size & position on close + MAP & SNR update only via KiwiSDR.com/public now + nognss.py script added, to remove GNSS ticks from the IQs so files can get opened and demodulated fine 128 | * v7.01: Bug fix on map update because of a single KiwiSDR node using https (ofc it just happened after the v7.00 release - haha) 129 | * v7.02: Small but important fix 130 | * v7.10: Bug fix on Start/Stop Listen function + TKinter exception at start fixed + modified patches for wavreader.py & tdoa_plot_map.m + more map POIs + more recording mode choices + legend showing current TDoA algo + files directories shortcut menu + some ultimateTDoA interface improvements 131 | * v7.20: TDoA broken because of early 2024 software mods, a bug was fixed in kiwirecorder.py (credit: Christoph Mayer, Thanks !) - only kiwirecorder_patch.diff has been modified (we will still use an old kiwiclient version) - apparently Listen mode is not working anymore - this version is only for bug correction. 132 | --- 133 | ## Thanks 134 | * Christoph Mayer @ https://github.com/hcab14/TDoA for the main TDoA code, excellent work and thanks for the public release ! 135 | * John Seamons, KiwiSDR developper @ https://github.com/jks-prv 136 | * Dmitry Janushkevich @ https://github.com/dev-zzo/kiwiclient for the code that I've modified to work with the GUI 137 | * Marco Cogoni (IS0KYB) for the microkiwi_waterfall.py code that I've modified to work directly via python 138 | * Nicolas M. for the MAC OS X directTDoA installing procedure 139 | 140 | ## Special Thanks 141 | * Daniel Ekmann, naturally designated as a beta tester, for the help on coding, feedbacks and suggestions since the beginning. 142 | -------------------------------------------------------------------------------- /directTDoA_server_list.db: -------------------------------------------------------------------------------- 1 | [ 2 | {"mac": "9059af5c2844", "url": "kiwisdr.r7d.xyz:8073", "id": "ZS6ETA", "snr": "14", "lat": "-25.850000", "lon": "28.210000"}, 3 | {"mac": "6433db1f85a7", "url": "93.41.148.192:8073", "id": "JN70iq", "snr": "11", "lat": "40.670139", "lon": "14.719914"}, 4 | {"mac": "40bd32e94227", "url": "apx.twrmon.net:8073", "id": "FM05os", "snr": "17", "lat": "35.760000", "lon": "-78.790000"}, 5 | {"mac": "e415f6f74898", "url": "119.18.20.124:8073", "id": "VK3OQ", "snr": "15", "lat": "-35.808583", "lon": "144.222015"}, 6 | {"mac": "9059af5e0500", "url": "lounix.net:8073", "id": "KD4HSO", "snr": "14", "lat": "38.934530", "lon": "-94.656210"}, 7 | {"mac": "883f4aa8f046", "url": "kiwisdr.itfais.com:8073", "id": "KT4RS", "snr": "21", "lat": "36.500000", "lon": "-81.300000"}, 8 | {"mac": "74e1827b88ed", "url": "arcticsdr.proxy.kiwisdr.com:8073", "id": "KQ40qr", "snr": "16", "lat": "70.730000", "lon": "29.350000"}, 9 | {"mac": "38d2697cae0e", "url": "kphsdr.com:8072", "id": "CM88mc", "snr": "6", "lat": "38.100000", "lon": "-122.950000"}, 10 | {"mac": "78047383804f", "url": "173.48.154.170:8073", "id": "KA1GXR", "snr": "21", "lat": "42.260000", "lon": "-71.500000"}, 11 | {"mac": "1442fc0fd7c7", "url": "sdr.hfunderground.com:8078", "id": "W3HFU/6", "snr": "23", "lat": "39.704000", "lon": "-76.974000"}, 12 | {"mac": "4c3fd33a816e", "url": "kiwibsb.ddns.net:8073", "id": "PT2FHC", "snr": "22", "lat": "-15.710000", "lon": "-47.820000"}, 13 | {"mac": "38d2697cc250", "url": "ciw321.cfars.ca:8174", "id": "VE6JY2", "snr": "16", "lat": "53.740000", "lon": "-112.820000"}, 14 | {"mac": "6433db38a07b", "url": "81.166.137.148:8073", "id": "JO28tu", "snr": "13", "lat": "58.843300", "lon": "5.597590"}, 15 | {"mac": "985dad48b1b1", "url": "kiwisdr.yensaw.net:8073", "id": "IO77VQ", "snr": "10", "lat": "57.695916", "lon": "-4.246218"}, 16 | {"mac": "985dad2b5d9d", "url": "rio.f5.si:8073", "id": "JA1GJD", "snr": "12", "lat": "35.175205", "lon": "137.040611"}, 17 | {"mac": "985dad810cef", "url": "la3rk.dyndns.org:8073", "id": "LA3RK", "snr": "34", "lat": "59.938200", "lon": "10.636200"}, 18 | {"mac": "e0623466fa5c", "url": "kiwisdr2541.ddns.net:8073", "id": "G7JHU", "snr": "11", "lat": "53.230000", "lon": "0.330000"}, 19 | {"mac": "6433db5a795e", "url": "hjf2021.ddns.net:8073", "id": "IN50tf", "snr": "25", "lat": "40.235000", "lon": "-8.415000"}, 20 | {"mac": "1442fc0f7d58", "url": "86.209.103.192:8073", "id": "LZ1AQ", "snr": "23", "lat": "46.426400", "lon": "0.871400"}, 21 | {"mac": "b0d5cc571478", "url": "no1dsdr.ddns.net:8073", "id": "NO1D", "snr": "9", "lat": "34.569542", "lon": "-112.338320"}, 22 | {"mac": "40bd32e51c9f", "url": "sdr.hfunderground.com:8077", "id": "W3HFU/5", "snr": "24", "lat": "39.704000", "lon": "-76.974000"}, 23 | {"mac": "6433db5a6a3a", "url": "kiwi.wlansupport.no:8073", "id": "LA6LLA", "snr": "26", "lat": "58.646000", "lon": "8.375000"}, 24 | {"mac": "c4f312b9ff6f", "url": "fraso.ddns.net:8073", "id": "JN37MC", "snr": "11", "lat": "47.090000", "lon": "7.070000"}, 25 | {"mac": "40bd32e94614", "url": "kiwisdr-iz0ina.ns0.it:8073", "id": "IZ0INA", "snr": "32", "lat": "41.910000", "lon": "12.580000"}, 26 | {"mac": "40bd32c8d04e", "url": "dj9kaikiwi.ddns.net:8073", "id": "DJ9KAI", "snr": "21", "lat": "48.784060", "lon": "9.151472"}, 27 | {"mac": "40bd32e54864", "url": "80.88.23.229:8073", "id": "JO50kq", "snr": "13", "lat": "50.690000", "lon": "10.910000"}, 28 | {"mac": "883f4a94b3f0", "url": "kiwisdr.inf.dhbw-ravensburg.de:8073", "id": "DK0TE/JN47rp", "snr": "25", "lat": "47.665021", "lon": "9.448193"}, 29 | {"mac": "40bd32c8d4cb", "url": "136.35.211.245:8073", "id": "EM29QE", "snr": "11", "lat": "39.200000", "lon": "-94.640000"}, 30 | {"mac": "883f4aabafa2", "url": "kiwisdrtrebor.ddns.net:8073", "id": "JO32mk", "snr": "20", "lat": "52.440000", "lon": "7.050000"}, 31 | {"mac": "3403de5f5808", "url": "railgun.proxy.kiwisdr.com:8073", "id": "PL05sa", "snr": "18", "lat": "25.040854", "lon": "121.518549"}, 32 | {"mac": "884aeaf59cc6", "url": "kiwisdr.areg.org.au:8074", "id": "VK5ARG", "snr": "32", "lat": "-34.273700", "lon": "138.771000"}, 33 | {"mac": "38d2697cc24a", "url": "81.207.169.26:8073", "id": "PA1KE", "snr": "20", "lat": "51.730000", "lon": "5.300000"}, 34 | {"mac": "884aeaf55ae5", "url": "47.188.174.170:8073", "id": "N5HYH", "snr": "11", "lat": "32.960000", "lon": "-97.090000"}, 35 | {"mac": "20d7787dc6ef", "url": "barossa.servebeer.com:8073", "id": "PF95ml", "snr": "18", "lat": "-34.510000", "lon": "139.050000"}, 36 | {"mac": "0035ff9c9e94", "url": "lb1pj-kiwisdr.hopto.org:8073", "id": "JO49ue", "snr": "22", "lat": "59.190928", "lon": "9.715149"}, 37 | {"mac": "883f4aa8cf13", "url": "kiwisdrknoxville.ddns.net:8073", "id": "AI4FU", "snr": "15", "lat": "36.000016", "lon": "-83.827475"}, 38 | {"mac": "883f4aa8e0aa", "url": "meinsdr.ddns.net:8073", "id": "DL7APJ", "snr": "24", "lat": "52.396902", "lon": "13.276442"}, 39 | {"mac": "38d2697cd2d3", "url": "nordheide.proxy.kiwisdr.com:8073", "id": "JO43UG", "snr": "20", "lat": "53.280939", "lon": "9.712275"}, 40 | {"mac": "985dad4533a3", "url": "fsdr.duckdns.org:80", "id": "SZFVAR", "snr": "21", "lat": "47.150000", "lon": "18.390000"}, 41 | {"mac": "e415f6f6fd1a", "url": "jk1lot.ham-radio-op.net:8073", "id": "JK1LOT", "snr": "27", "lat": "35.540000", "lon": "139.460000"}, 42 | {"mac": "985dad49294b", "url": "data3.caprockweather.com:8073", "id": "N5PYK", "snr": "13", "lat": "33.600000", "lon": "-101.900000"}, 43 | {"mac": "883f4aa8c5c8", "url": "bikedork.myddns.me:8073", "id": "BIKEDORK", "snr": "8", "lat": "44.910000", "lon": "-93.280000"}, 44 | {"mac": "883f4aa8e0a7", "url": "kb0fxkiwisdr.hopto.org:8073", "id": "KB0FX", "snr": "15", "lat": "38.850000", "lon": "-91.200000"}, 45 | {"mac": "e415f6f74598", "url": "pallas-kiwi.ddns.net:8073", "id": "KP28be", "snr": "16", "lat": "68.170694", "lon": "24.157444"}, 46 | {"mac": "3403de8b3421", "url": "kiwisdr.serveftp.com:8073", "id": "JO31rl", "snr": "33", "lat": "51.497878", "lon": "7.426248"}, 47 | {"mac": "b0d5cc5711f4", "url": "foxgulch.proxy.kiwisdr.com:8073", "id": "W0AY", "snr": "2", "lat": "46.523234", "lon": "-114.114358"}, 48 | {"mac": "74e182a80d93", "url": "jq1zyv.ddns.net:80", "id": "QM06gi", "snr": "16", "lat": "36.367547", "lon": "140.562087"}, 49 | {"mac": "985dad7f7aeb", "url": "kiwib.la2zoa.net:8073", "id": "LA2ZOA/B", "snr": "25", "lat": "59.890000", "lon": "10.520000"}, 50 | {"mac": "84eb189bc646", "url": "kiwisdr.vk3tlw.net:8073", "id": "VK3TLW", "snr": "9", "lat": "-37.853689", "lon": "145.132825"}, 51 | {"mac": "74e18276045c", "url": "jp1odj.proxy.kiwisdr.com:8073", "id": "JP1ODJ", "snr": "19", "lat": "36.008373", "lon": "139.544020"}, 52 | {"mac": "689e1985cec0", "url": "sm2byc.ddns.net:8073", "id": "SM2BYC/1", "snr": "24", "lat": "65.965298", "lon": "24.030000"}, 53 | {"mac": "985dad48ecc1", "url": "80.75.112.98:8073", "id": "HB9NF", "snr": "10", "lat": "47.328590", "lon": "8.961670"}, 54 | {"mac": "883f4a9a75f2", "url": "oe1xtu.proxy.kiwisdr.com:8073", "id": "OE1XTU", "snr": "34", "lat": "48.196600", "lon": "16.371100"}, 55 | {"mac": "6433db49bc88", "url": "45.84.170.14:8073", "id": "JN60wr", "snr": "29", "lat": "40.730510", "lon": "13.906815"}, 56 | {"mac": "883f4aa8c5e6", "url": "taudel.ddns.net:8073", "id": "HB9CJJ", "snr": "15", "lat": "47.361472", "lon": "8.112919"}, 57 | {"mac": "40bd32e94608", "url": "f6bir.ddns.net:8073", "id": "f6bir", "snr": "14", "lat": "49.220000", "lon": "2.210000"}, 58 | {"mac": "883f4aa8be0f", "url": "sdr-amradioantennas.com:8075", "id": "VK3KHZ/5", "snr": "9", "lat": "-37.820000", "lon": "145.280000"}, 59 | {"mac": "c4f312a0a6ef", "url": "websdr.uk:8076", "id": "G4SZM", "snr": "37", "lat": "51.317266", "lon": "-2.950479"}, 60 | {"mac": "0035ff9cf603", "url": "9k2ra-3.proxy.kiwisdr.com:8073", "id": "LL39xi", "snr": "21", "lat": "29.364500", "lon": "47.988899"}, 61 | {"mac": "1442fc0f664e", "url": "77.223.174.203:8073", "id": "LB3J", "snr": "22", "lat": "63.426353", "lon": "8.146556"}, 62 | {"mac": "985dad7f14d9", "url": "witikiwi.ddns.net:8073", "id": "OE5EAN", "snr": "20", "lat": "48.151842", "lon": "13.998109"}, 63 | {"mac": "0035ff989f2e", "url": "w9adkiwi.hopto.org:8073", "id": "W9AD", "snr": "12", "lat": "42.140000", "lon": "-88.200000"}, 64 | {"mac": "0035ff9b915d", "url": "pallas-kiwi-2.ddns.net:8074", "id": "KP28be/4", "snr": "1", "lat": "68.170694", "lon": "24.157444"}, 65 | {"mac": "3403de939828", "url": "vk3cmz.ddns.net:8073", "id": "VK3KKP", "snr": "10", "lat": "-37.065100", "lon": "144.220400"}, 66 | {"mac": "38d2697ccb5b", "url": "ams.dedyn.io:8073", "id": "JN49em", "snr": "20", "lat": "49.530000", "lon": "8.390000"}, 67 | {"mac": "9884e38cd47c", "url": ":8073", "id": "LA5F", "snr": "35", "lat": "59.090000", "lon": "10.930000"}, 68 | {"mac": "40bd32c8d9f3", "url": "94.214.128.124:8073", "id": "PE1OPQ", "snr": "27", "lat": "53.080000", "lon": "6.800000"}, 69 | {"mac": "b8804f25ac6c", "url": "68.14.214.57:8073", "id": "W7DJS", "snr": "8", "lat": "33.626471", "lon": "-112.433728"}, 70 | {"mac": "0035ff98d084", "url": "kiwieriz.dyndns.org:8073", "id": "JN36us", "snr": "18", "lat": "46.791090", "lon": "7.741990"}, 71 | {"mac": "884aeaf5ad46", "url": "kernow.hopto.org:8073", "id": "Kernow", "snr": "28", "lat": "50.048000", "lon": "-5.182000"}, 72 | {"mac": "985dad7f2a38", "url": "iw2nke.ddns.net:8073", "id": "MarkerIT06", "snr": "54", "lat": "43.662612", "lon": "13.136668"}, 73 | {"mac": "c4f312a05b41", "url": "204.235.44.74:8073", "id": "WS5W", "snr": "10", "lat": "33.360000", "lon": "-96.620000"}, 74 | {"mac": "0035ff9cf990", "url": "kiwi5167.ddns.net:8073", "id": "DN06je", "snr": "17", "lat": "46.170000", "lon": "-119.170000"}, 75 | {"mac": "40bd322eb47c", "url": "vr2bg.proxy.kiwisdr.com:8073", "id": "VR2BG", "snr": "12", "lat": "22.350000", "lon": "114.130000"}, 76 | {"mac": "b8804f25ac18", "url": "boomerthedog.com:8073", "id": "EN90xk", "snr": "12", "lat": "40.420000", "lon": "-80.040000"}, 77 | {"mac": "780473392d93", "url": "lodsdr.proxy.kiwisdr.com:8073", "id": "SM5VXO", "snr": "15", "lat": "58.380000", "lon": "15.640000"}, 78 | {"mac": "985dad7f5e4b", "url": "z2abpyweepil7rhq.myfritz.net:8074", "id": "DC9FD", "snr": "34", "lat": "50.540000", "lon": "9.460000"}, 79 | {"mac": "38d2697ccb6d", "url": "raleigh.twrmon.net:8073", "id": "FM05qx", "snr": "22", "lat": "35.970000", "lon": "-78.650000"}, 80 | {"mac": "883f4a9a96dd", "url": "hasenberg700m.ddns.net:8076", "id": "JN47ej", "snr": "16", "lat": "47.378185", "lon": "8.365395"}, 81 | {"mac": "b8804f25b071", "url": "79.2.157.173:8073", "id": "I5HGM", "snr": "38", "lat": "43.522711", "lon": "11.477597"}, 82 | {"mac": "0035ff622cbf", "url": "mvradio.org:8073", "id": "Methow", "snr": "9", "lat": "48.478441", "lon": "-120.190905"}, 83 | {"mac": "883f4aabbd88", "url": "kc9ffvkiwisdr.gotdns.com:8073", "id": "KC9FFV", "snr": "19", "lat": "32.765568", "lon": "-96.457835"}, 84 | {"mac": "b827ebda93f7", "url": "tredxk.no-ip.org:8074", "id": "KP11wm", "snr": "25", "lat": "61.528000", "lon": "23.848000"}, 85 | {"mac": "88c2556b3d70", "url": "pardinho.kiwisdr.com.br:8073", "id": "GG56tv", "snr": "12", "lat": "-23.110000", "lon": "-48.380000"}, 86 | {"mac": "e06234437fef", "url": "dl4eeckiwi.proxy.kiwisdr.com:8073", "id": "DL4EECKiwi", "snr": "32", "lat": "53.628994", "lon": "7.906808"}, 87 | {"mac": "9884e3938d35", "url": "91.149.49.175:8073", "id": "LA3SP", "snr": "17", "lat": "60.628128", "lon": "8.175728"}, 88 | {"mac": "6433db1ba09c", "url": "th-thamai.feste-ip.net:8073", "id": "OK12ao", "snr": "18", "lat": "12.620000", "lon": "102.010000"}, 89 | {"mac": "780473388770", "url": "sdr-amradioantennas.com:8076", "id": "VK3KHZ/6", "snr": "10", "lat": "-37.817000", "lon": "145.288000"}, 90 | {"mac": "0035ff9baf06", "url": "vk2ggc2.ddns.net:8074", "id": "VK2GGC", "snr": "34", "lat": "-32.525000", "lon": "151.754400"}, 91 | {"mac": "402e71ec70f8", "url": "g3sdh.com:8054", "id": "IO81qh", "snr": "29", "lat": "51.310000", "lon": "-2.650000"}, 92 | {"mac": "e415f6f748da", "url": "de1lon.dd-dns.de:8073", "id": "DE1LON", "snr": "24", "lat": "51.140000", "lon": "6.700000"}, 93 | {"mac": "b0d5cc5723d1", "url": "kiwicam.sk3w.se:8073", "id": "SA0CAM", "snr": "28", "lat": "59.460112", "lon": "17.884806"}, 94 | {"mac": "c4f312b9ff12", "url": "argyllsdr.ddns.net:8073", "id": "GM8XBZ", "snr": "23", "lat": "55.910000", "lon": "-5.240000"}, 95 | {"mac": "e415f6f761d3", "url": "45.73.0.50:8073", "id": "FN35dn", "snr": "21", "lat": "45.554785", "lon": "-73.670049"}, 96 | {"mac": "40bd322e9bbf", "url": "kiwiik2wbg.ddns.net:8073", "id": "IK2WBG", "snr": "18", "lat": "45.800000", "lon": "9.130000"}, 97 | {"mac": "78047338d12c", "url": "websdr.uk:8077", "id": "G4SZM/7", "snr": "39", "lat": "51.317266", "lon": "-2.950479"}, 98 | {"mac": "0035ff98d9d2", "url": "5.102.161.100:8073", "id": "JO50gx", "snr": "16", "lat": "50.999256", "lon": "10.581883"}, 99 | {"mac": "985dad4921e2", "url": "31.187.212.94:8073", "id": "PA1W", "snr": "10", "lat": "51.787787", "lon": "5.936868"}, 100 | {"mac": "80f5b52d1ac6", "url": "sdr.w5tsu.net:8073", "id": "W5TSU", "snr": "13", "lat": "35.533180", "lon": "-97.621357"}, 101 | {"mac": "80f5b5f563a3", "url": "iceharbor5815.ddns.net:8073", "id": "DN06nf", "snr": "16", "lat": "46.242692", "lon": "-118.875750"}, 102 | {"mac": "40bd32c8d2ac", "url": "do1dbs.dd-dns.de:8073", "id": "DO1DBS", "snr": "25", "lat": "51.555956", "lon": "7.381721"}, 103 | {"mac": "c4f312a08754", "url": "74.82.153.108:8073", "id": "ND7M", "snr": "15", "lat": "36.240000", "lon": "-116.060000"}, 104 | {"mac": "985dad4279a3", "url": "tranp.sytes.net:8073", "id": "Hanoi", "snr": "11", "lat": "21.037490", "lon": "105.812904"}, 105 | {"mac": "38d2697ce597", "url": "sdr-amradioantennas.com:8073", "id": "VK3KHZ/3", "snr": "11", "lat": "-37.817000", "lon": "145.288000"}, 106 | {"mac": "e415f6f745fb", "url": "77.162.242.103:8073", "id": "JO21gv", "snr": "21", "lat": "51.881980", "lon": "4.542561"}, 107 | {"mac": "985dad7f1b21", "url": "fieldstation.no-ip.biz:8073", "id": "DL7APJ/3", "snr": "30", "lat": "52.396902", "lon": "13.276442"}, 108 | {"mac": "0035ff994d4c", "url": "109.164.114.15:8073", "id": "CSDX", "snr": "31", "lat": "49.940000", "lon": "12.560000"}, 109 | {"mac": "985dad7f0de1", "url": "g4bkhkiwi.ddns.net:8073", "id": "G4BKH", "snr": "14", "lat": "53.540000", "lon": "-2.140000"}, 110 | {"mac": "985dad7f366a", "url": "99.3.11.63:8073", "id": "WB8CXO", "snr": "20", "lat": "41.150000", "lon": "-81.430000"}, 111 | {"mac": "883f4a9a109a", "url": "212.225.223.202:8073", "id": "IM77ow", "snr": "20", "lat": "37.950000", "lon": "-4.810000"}, 112 | {"mac": "c4f31281ccbf", "url": "kiwisdr.areg.org.au:8073", "id": "VK5ARG/3", "snr": "28", "lat": "-34.273700", "lon": "138.771000"}, 113 | {"mac": "985dad7f7443", "url": "f1jeksdr1.ddns.net:8073", "id": "F1JEK", "snr": "31", "lat": "45.790000", "lon": "0.650000"}, 114 | {"mac": "40bd32e30ee2", "url": "picamiolos.dynns.com:8073", "id": "hb9tpc", "snr": "0", "lat": "40.710000", "lon": "-8.050000"}, 115 | {"mac": "0035ff9dbe2b", "url": "sdrwales.hopto.org:8073", "id": "IO72wn", "snr": "19", "lat": "52.580000", "lon": "-4.090000"}, 116 | {"mac": "0cb2b7d6cdb0", "url": "westernmd.proxy.kiwisdr.com:8073", "id": "KB3CS", "snr": "13", "lat": "39.600000", "lon": "-78.900000"}, 117 | {"mac": "c4f312a0873f", "url": "planet3.dyndns.org:8073", "id": "G8AOE1", "snr": "23", "lat": "54.510000", "lon": "-1.330000"}, 118 | {"mac": "6433db465dc9", "url": "f5nkp.freeboxos.fr:8073", "id": "F5NKP", "snr": "26", "lat": "48.822431", "lon": "2.252981"}, 119 | {"mac": "e415f6fd892d", "url": "gw0kig.ddns.net:8073", "id": "IO81hr", "snr": "16", "lat": "51.747940", "lon": "-3.377790"}, 120 | {"mac": "38d2697cc695", "url": "sbs1suisse.internet-box.ch:8074", "id": "JN47ej/4", "snr": "25", "lat": "47.378175", "lon": "8.365381"}, 121 | {"mac": "b0d5ccfc816b", "url": "sielsdr.ddns.net:8073", "id": "DF0KL", "snr": "30", "lat": "53.665337", "lon": "7.282766"}, 122 | {"mac": "9884e38ccfad", "url": "0kqvydic4s76vnto.myfritz.net:8073", "id": "DO2STK", "snr": "23", "lat": "54.507518", "lon": "8.993393"}, 123 | {"mac": "0035ff9cb4a2", "url": "g3sdh.com:8053", "id": "IO81qh/3", "snr": "31", "lat": "51.310000", "lon": "-2.650000"}, 124 | {"mac": "985dadd0af03", "url": "pdbh2d76wb7bt5z2.myfritz.net:8073", "id": "DL3ZID", "snr": "32", "lat": "53.599300", "lon": "11.328700"}, 125 | {"mac": "40bd32e4deb3", "url": "hb9flq.ham-radio-op.net:8073", "id": "JN47jk", "snr": "12", "lat": "47.424539", "lon": "8.812500"}, 126 | {"mac": "780473a94c5f", "url": "zl1mga.zapto.org:8073", "id": "ZL1ROT", "snr": "20", "lat": "-38.030000", "lon": "176.200000"}, 127 | {"mac": "884aeaf5b7b0", "url": "kiwisdr.hameleers.net:8073", "id": "JO21JN", "snr": "15", "lat": "51.570000", "lon": "4.780000"}, 128 | {"mac": "40bd32e4da12", "url": "on5pvd.dyndns.info:8073", "id": "ON5PVD", "snr": "20", "lat": "50.910000", "lon": "4.290000"}, 129 | {"mac": "985dad7f809e", "url": "kiwisdr.lollisoft.de:8073", "id": "JO40gd", "snr": "7", "lat": "50.162217", "lon": "8.553844"}, 130 | {"mac": "985dad48e1b0", "url": "ha6smfkiwi.proxy.kiwisdr.com:8073", "id": "HA6SMF", "snr": "29", "lat": "47.740000", "lon": "20.420000"}, 131 | {"mac": "b0d5cc571400", "url": "dentonhill-sdr.moses.bz:80", "id": "W8FSM", "snr": "13", "lat": "42.776054", "lon": "-83.706163"}, 132 | {"mac": "e06234439ace", "url": "176.155.231.43:8073", "id": "JN39ak", "snr": "23", "lat": "49.440000", "lon": "6.080000"}, 133 | {"mac": "40bd32e4f962", "url": "65.60.166.216:8073", "id": "EN80md", "snr": "18", "lat": "40.125100", "lon": "-82.931500"}, 134 | {"mac": "38d269837eaf", "url": "sdr-amradioantennas.com:8072", "id": "VK3KHZ/2", "snr": "10", "lat": "-37.817000", "lon": "145.288000"}, 135 | {"mac": "544a16bbe1cb", "url": "kiwi8073.ddns.net:8074", "id": "IV3HZR", "snr": "24", "lat": "45.606785", "lon": "13.744692"}, 136 | {"mac": "780473505c0d", "url": "149.28.25.218:8073", "id": "DU6/PE1NSQ", "snr": "25", "lat": "10.723531", "lon": "122.973989"}, 137 | {"mac": "985dad7f2a14", "url": "84.228.126.167:8073", "id": "Z7GUL", "snr": "10", "lat": "32.178200", "lon": "34.907600"}, 138 | {"mac": "e062346fdfef", "url": "km4rt.ddns.net:8073", "id": "KM4RT", "snr": "9", "lat": "35.448000", "lon": "-89.653000"}, 139 | {"mac": "40bd32c8d6ab", "url": "zubi.proxy.kiwisdr.com:8073", "id": "JN49ji", "snr": "14", "lat": "49.342221", "lon": "8.752874"}, 140 | {"mac": "9059af7e9491", "url": "rixon.get-o.net:80", "id": "K0DEZ", "snr": "19", "lat": "39.220000", "lon": "-94.600000"}, 141 | {"mac": "780473834f0e", "url": "kiwisdr.lamont.me.uk:8073", "id": "G4DYA/1", "snr": "29", "lat": "52.900000", "lon": "-2.100000"}, 142 | {"mac": "c8a030ab6fad", "url": "kiwisdr.briata.org:8073", "id": "I1CRA", "snr": "28", "lat": "44.782900", "lon": "8.529038"}, 143 | {"mac": "b827ebe743eb", "url": "swiss.ham-radio-op.net:8075", "id": "HE9JAP", "snr": "13", "lat": "46.552000", "lon": "6.642000"}, 144 | {"mac": "e415f6f74af0", "url": "kd7nfr.freeddns.org:8073", "id": "KD7NFR", "snr": "5", "lat": "31.040000", "lon": "-83.190000"}, 145 | {"mac": "40bd322ebe12", "url": "kiwisdr.oh6ai.fi:8073", "id": "OH6LSL", "snr": "29", "lat": "63.835500", "lon": "22.957000"}, 146 | {"mac": "38d269837ea9", "url": "ahorn.proxy.kiwisdr.com:8073", "id": "HB9DSE", "snr": "39", "lat": "47.050000", "lon": "7.870000"}, 147 | {"mac": "78047338fe86", "url": "hasenberg700m.ddns.net:8073", "id": "JN47ej/3", "snr": "15", "lat": "47.378185", "lon": "8.365395"}, 148 | {"mac": "985dad7f50ce", "url": "os.f5.si:8073", "id": "PM74tu", "snr": "13", "lat": "34.850000", "lon": "135.590000"}, 149 | {"mac": "883f4aa8cf37", "url": "websdr.uk:8060", "id": "IO81mh", "snr": "41", "lat": "51.317266", "lon": "-2.950479"}, 150 | {"mac": "780473388725", "url": "kiwisdr.tgcfabian.nl:80", "id": "TGCFabian", "snr": "14", "lat": "52.516775", "lon": "6.083022"}, 151 | {"mac": "883f4a99ff09", "url": "websdr.uk:80", "id": "G4SZM/0", "snr": "41", "lat": "51.317266", "lon": "-2.950479"}, 152 | {"mac": "1442fc0f6d4a", "url": "hnswerkstatt.dyndns.org:8073", "id": "h13RF029", "snr": "35", "lat": "51.890000", "lon": "12.760000"}, 153 | {"mac": "1442fc0f6984", "url": "kiwisdr2.hoka.co.uk:8073", "id": "STNB", "snr": "17", "lat": "51.292581", "lon": "0.979165"}, 154 | {"mac": "1862e4d0c3f3", "url": "190.123.83.42:8073", "id": "LU1HCW", "snr": "25", "lat": "-32.660000", "lon": "-64.760000"}, 155 | {"mac": "c4f312753dae", "url": "kiwi-vih.aprs.fi:8073", "id": "KP20/aprsfi", "snr": "19", "lat": "63.075000", "lon": "27.275000"}, 156 | {"mac": "b8804f327a29", "url": "dl0dtm.ddns.net:8073", "id": "DL0DTM", "snr": "20", "lat": "50.723782", "lon": "7.145407"}, 157 | {"mac": "0035ff9b68bf", "url": "azzg6sujivyua29f.myfritz.net:8073", "id": "JO53cl", "snr": "31", "lat": "53.480000", "lon": "10.190000"}, 158 | {"mac": "40bd32e2d990", "url": "plonsk.proxy.kiwisdr.com:8073", "id": "SDRPlonsk", "snr": "39", "lat": "52.620000", "lon": "20.368000"}, 159 | {"mac": "28ec9af27714", "url": "air.swl.su:80", "id": "KO85rw", "snr": "0", "lat": "55.917158", "lon": "37.418104"}, 160 | {"mac": "6433db11f7dc", "url": "pe1rdp.no-ip.org:8073", "id": "PE1RDP", "snr": "19", "lat": "51.441661", "lon": "5.374092"}, 161 | {"mac": "b0d5cc571b4e", "url": "kiwisdr.aprsinfo.com:8073", "id": "M0XDK", "snr": "20", "lat": "52.186065", "lon": "-0.895373"}, 162 | {"mac": "1442fc0c8dc9", "url": "177.ham-radio-op.net:8073", "id": "JO01mi", "snr": "15", "lat": "51.355177", "lon": "1.017452"}, 163 | {"mac": "38d2697cc211", "url": "s4wmv4hovyls5ebz.myfritz.net:8075", "id": "SWLJO43/3", "snr": "37", "lat": "53.307000", "lon": "9.987400"}, 164 | {"mac": "883f4aa8cf0a", "url": "kr6la.proxy.kiwisdr.com:8073", "id": "CN90ao", "snr": "12", "lat": "40.620000", "lon": "-121.920000"}, 165 | {"mac": "04a316af0eed", "url": "thomas0177.dnshome.de:8073", "id": "DL0073SWL", "snr": "24", "lat": "52.418663", "lon": "13.306890"}, 166 | {"mac": "b8804f25c4de", "url": "99.102.143.126:8073", "id": "KU4SD", "snr": "16", "lat": "34.010000", "lon": "-83.490000"}, 167 | {"mac": "c4f312a678db", "url": "ve3hoa.ddns.net:8073", "id": "VE3HOA", "snr": "22", "lat": "45.270000", "lon": "-76.050000"}, 168 | {"mac": "f045da7def63", "url": "sm2byc.ddns.net:8074", "id": "SM2BYC/2", "snr": "25", "lat": "65.965298", "lon": "24.03"}, 169 | {"mac": "38d2697cae38", "url": "nutronix.dd-dns.de:8073", "id": "OE9NGH", "snr": "2", "lat": "47.464298", "lon": "9.907030"}, 170 | {"mac": "c4f312b9faaa", "url": "websdr.uk:8074", "id": "G4SZM/4", "snr": "41", "lat": "51.317266", "lon": "-2.950479"}, 171 | {"mac": "7804733915f3", "url": "plonsk2.proxy.kiwisdr.com:8073", "id": "SDRPlonsk/3", "snr": "41", "lat": "52.620000", "lon": "20.368000"}, 172 | {"mac": "e06234438865", "url": "sm4fge.ddns.net:8073", "id": "SM4FGE", "snr": "31", "lat": "60.070000", "lon": "15.140000"}, 173 | {"mac": "6433db48e8b4", "url": "bv3un.ddns.net:8073", "id": "BV3UN", "snr": "9", "lat": "25.052481", "lon": "121.297046"}, 174 | {"mac": "04a316b0a8d8", "url": "jp1odj-air.proxy.kiwisdr.com:8073", "id": "JP1ODJ/3", "snr": "0", "lat": "36.038350", "lon": "139.314034"}, 175 | {"mac": "0035ff89db87", "url": "oe3wyc.ddns.net:8073", "id": "OE1WYC", "snr": "29", "lat": "48.218000", "lon": "16.466290"}, 176 | {"mac": "985dad491114", "url": "k2zn.ddns.net:8073", "id": "K2ZN", "snr": "12", "lat": "43.120000", "lon": "-77.620000"}, 177 | {"mac": "b8804f25d25b", "url": "81.92.181.198:8073", "id": "JO33bg", "snr": "25", "lat": "53.280789", "lon": "6.088008"}, 178 | {"mac": "b0d5cc571679", "url": "ik2biy.proxy.kiwisdr.com:8073", "id": "IK2BIY", "snr": "27", "lat": "45.709315", "lon": "10.212806"}, 179 | {"mac": "6433db391ce6", "url": "kiwi-kuo.aprs.fi:8073", "id": "KP33pb", "snr": "23", "lat": "63.075000", "lon": "27.275000"}, 180 | {"mac": "884aeaf5858e", "url": "kiwisdr.surriel.com:80", "id": "AB1KW", "snr": "28", "lat": "43.170715", "lon": "-71.566939"}, 181 | {"mac": "3403de631d0d", "url": "sdr.ironstonerange.com:8073", "id": "VK5PH", "snr": "29", "lat": "-34.964437", "lon": "138.762380"}, 182 | {"mac": "985dad812e70", "url": "sdrtoyb.ddns.net:8073", "id": "JP40nn", "snr": "9", "lat": "60.568060", "lon": "9.102760"}, 183 | {"mac": "78047338da92", "url": "plonsk3.proxy.kiwisdr.com:8073", "id": "SDRPlonsk/3", "snr": "38", "lat": "52.620000", "lon": "20.368000"}, 184 | {"mac": "884aeae013ab", "url": "sdr.n3ka.com:8073", "id": "N3LGA", "snr": "12", "lat": "37.370000", "lon": "-122.026000"}, 185 | {"mac": "04a316df1bca", "url": "linkz.ddns.net:8073", "id": "linkz", "snr": "31", "lat": "45.402767", "lon": "5.277375"}, 186 | {"mac": "3403de8df4b8", "url": "w9xa.us:8073", "id": "W9XA", "snr": "16", "lat": "41.852336", "lon": "-88.327603"}, 187 | {"mac": "38d2693e89d5", "url": "irk.swl.su:80", "id": "OO22eg", "snr": "19", "lat": "52.277911", "lon": "104.359891"}, 188 | {"mac": "40bd32e55512", "url": "sigmasdr.ddns.net:8073", "id": "N1NTE/1", "snr": "25", "lat": "42.033943", "lon": "-72.140461"}, 189 | {"mac": "e062346753e1", "url": "sbs1suisse.internet-box.ch:8073", "id": "JN47di", "snr": "0", "lat": "47.351333", "lon": "8.271333"}, 190 | {"mac": "b0d5ccff4b38", "url": "rgv2.twrmon.net:8074", "id": "EL15gw", "snr": "13", "lat": "25.934624", "lon": "-97.462922"}, 191 | {"mac": "40bd32e54d17", "url": "rgv.twrmon.net:8075", "id": "EL15gw/5", "snr": "13", "lat": "25.930000", "lon": "-97.462921"}, 192 | {"mac": "985dad7f31da", "url": "72.235.217.245:8073", "id": "NH6XO", "snr": "8", "lat": "21.403000", "lon": "-157.807000"}, 193 | {"mac": "1442fc0f98c1", "url": "bv7au.ddns.net:8073", "id": "BV7AU", "snr": "0", "lat": "22.790000", "lon": "120.580000"}, 194 | {"mac": "883f4a998b86", "url": "sdrbcn.duckdns.org:8073", "id": "EA3HRU", "snr": "15", "lat": "41.411360", "lon": "1.999500"}, 195 | {"mac": "40bd322ec32b", "url": "linkz.ddns.net:8074", "id": "lnkz", "snr": "39", "lat": "45.402778", "lon": "5.277473"}, 196 | {"mac": "780473832a84", "url": "warszawa.proxy.kiwisdr.com:8073", "id": "KO02lg", "snr": "18", "lat": "52.259885", "lon": "20.957187"}, 197 | {"mac": "883f4aab90d6", "url": "kf2da.ddns.net:8073", "id": "KF2DA", "snr": "16", "lat": "42.080000", "lon": "-76.820000"}, 198 | {"mac": "b8804f25b077", "url": "db0bbb.dnshome.de:8073", "id": "JO62sq", "snr": "20", "lat": "52.672911", "lon": "13.547363"}, 199 | {"mac": "e062346fcf69", "url": "kiwi-neuwied.ddns.net:8073", "id": "JO30QK", "snr": "21", "lat": "50.457916", "lon": "7.428928"}, 200 | {"mac": "b8804f25c0d9", "url": "kiwisdr-schwarzenburg.try.yaler.io:80", "id": "JN36qt", "snr": "16", "lat": "46.810606", "lon": "7.355672"}, 201 | {"mac": "e415f6f66b0d", "url": "scaeschi.ddns.net:8073", "id": "JN36UP", "snr": "18", "lat": "46.640000", "lon": "7.730000"}, 202 | {"mac": "e415f6f748f2", "url": "vk2ggc.ddns.net:8073", "id": "VK2GGC/3", "snr": "12", "lat": "-32.550000", "lon": "151.735000"}, 203 | {"mac": "84eb18e5377f", "url": "kiwi-sdr2-leiden.impactam.nl:8073", "id": "ImpactAM", "snr": "22", "lat": "52.150000", "lon": "4.450000"}, 204 | {"mac": "985dad49051b", "url": "14.8.66.227:5025", "id": "PM96ok", "snr": "21", "lat": "36.450000", "lon": "139.200000"}, 205 | {"mac": "c4f312a678c9", "url": "rx2.wa2zkd.net:8073", "id": "ZKD/Maine", "snr": "23", "lat": "44.170000", "lon": "-69.080000"}, 206 | {"mac": "e415f6f4ee16", "url": "ze30.internetdsl.tpnet.pl:8888", "id": "KO00sl", "snr": "19", "lat": "50.477006", "lon": "21.554581"}, 207 | {"mac": "c4f312724129", "url": "72.172.110.98:8175", "id": "VE6JY", "snr": "18", "lat": "53.740000", "lon": "-112.820000"}, 208 | {"mac": "38d2697ce5e5", "url": "javaradiofrance.ddns.net:8073", "id": "CX0006SWL", "snr": "7", "lat": "47.250153", "lon": "6.039819"}, 209 | {"mac": "e062346fe688", "url": "217.113.51.121:8073", "id": "HA6PX", "snr": "35", "lat": "48.128700", "lon": "19.883700"}, 210 | {"mac": "e415f6fd942e", "url": "47.54.214.91:8073", "id": "VY2HF", "snr": "11", "lat": "46.235520", "lon": "-63.082160"}, 211 | {"mac": "883f4aa8b614", "url": "kiwisdrik4cie.ddns.net:8073", "id": "IK4CIE", "snr": "28", "lat": "44.680000", "lon": "10.270000"}, 212 | {"mac": "b827ebb18685", "url": "iw3hbx.ddns.net:5555", "id": "IW3HBX", "snr": "13", "lat": "45.548833", "lon": "12.062231"}, 213 | {"mac": "c4f3127553a4", "url": "emeraldsdr.proxy.kiwisdr.com:8073", "id": "IO62nu", "snr": "5", "lat": "52.870000", "lon": "-6.850000"}, 214 | {"mac": "e062346fcf7b", "url": "graz.sytes.net:8073", "id": "OE6ADD", "snr": "35", "lat": "46.990000", "lon": "15.300000"}, 215 | {"mac": "b0d5ccf0df17", "url": "kiwisdr.k7biz.com:8073", "id": "K7BIZ", "snr": "7", "lat": "47.700000", "lon": "-116.770000"}, 216 | {"mac": "b0d5cc571db4", "url": "sv8rv.dyndns.org:8073", "id": "SV8RV", "snr": "18", "lat": "37.783103", "lon": "20.896453"}, 217 | {"mac": "40bd322ea078", "url": "kb1uif-kiwisdr.ddns.net:8073", "id": "KB1UIF", "snr": "14", "lat": "42.060000", "lon": "-73.310000"}, 218 | {"mac": "6433db1fa731", "url": "hanshomepa0ehg.ddns.net:8073", "id": "PA0EHG", "snr": "34", "lat": "52.684094", "lon": "7.519360"}, 219 | {"mac": "40bd32c8d4e0", "url": "la6lukiwisdr.ddns.net:8073", "id": "LA6LU", "snr": "26", "lat": "59.690000", "lon": "10.340000"}, 220 | {"mac": "c4f312b9fad7", "url": "websdr.uk:8073", "id": "G4SZM/3", "snr": "41", "lat": "51.317266", "lon": "-2.950479"}, 221 | {"mac": "b0d5cc570aac", "url": "take4radio.asuscomm.com:8073", "id": "PM84lw", "snr": "12", "lat": "34.920000", "lon": "136.990000"}, 222 | {"mac": "883f4a9a3c80", "url": "kiwionline.ddns.net:8073", "id": "JO31dh", "snr": "27", "lat": "51.296443", "lon": "6.273451"}, 223 | {"mac": "6433db175429", "url": "dl2kcl.selfhost.eu:8073", "id": "LZ1AQ/3", "snr": "28", "lat": "50.621803", "lon": "6.936500"}, 224 | {"mac": "40bd32e9462c", "url": "80.75.112.98:8075", "id": "HB9NF/5", "snr": "11", "lat": "47.328590", "lon": "8.961670"}, 225 | {"mac": "b827ebc60dc2", "url": "pso2.p.sdrotg.com:80", "id": "KotkaFinland", "snr": "28", "lat": "60.507676", "lon": "26.981506"}, 226 | {"mac": "985dad7efeca", "url": "mauisdr.wsprdaemon.org:8073", "id": "AI6VN", "snr": "18", "lat": "20.970000", "lon": "-156.540000"}, 227 | {"mac": "883f4aa8d939", "url": "hasenberg700m.ddns.net:8074", "id": "JN47ej/4", "snr": "15", "lat": "47.378185", "lon": "8.365395"}, 228 | {"mac": "38d2697cae44", "url": "209.115.233.71:8073", "id": "ve6ars1", "snr": "16", "lat": "50.080000", "lon": "-113.680000"}, 229 | {"mac": "b0d5cc5711dc", "url": "db0hal.dyndns.org:8073", "id": "DM2NT", "snr": "14", "lat": "51.466044", "lon": "11.977189"}, 230 | {"mac": "1442fc0f69b4", "url": "g7jur.ddns.net:8073", "id": "G7JUR", "snr": "24", "lat": "51.240304", "lon": "-0.786030"}, 231 | {"mac": "0035ff98e25a", "url": "jsgkiwi.ddns.net:8073", "id": "KP24sm", "snr": "17", "lat": "64.510000", "lon": "25.550000"}, 232 | {"mac": "0035ff98e248", "url": "217.26.107.148:8073", "id": "PA3GJX", "snr": "37", "lat": "52.603096", "lon": "5.950619"}, 233 | {"mac": "e415f6f745b3", "url": "sdr.k1vl.com:8073", "id": "K1VL", "snr": "28", "lat": "43.507697", "lon": "-72.816799"}, 234 | {"mac": "883f4aab7be8", "url": "62.170.14.132:8073", "id": "JN61vq", "snr": "16", "lat": "41.690000", "lon": "13.750000"}, 235 | {"mac": "c4f312b9ff51", "url": "f6abj-kiwihf.ddns.net:8073", "id": "F6ABJ", "snr": "22", "lat": "45.384028", "lon": "5.093461"}, 236 | {"mac": "40bd32e93def", "url": "WV5L-sdr.dynu.net:8073", "id": "EM74sc", "snr": "12", "lat": "34.107823", "lon": "-84.462645"}, 237 | {"mac": "3403de81254c", "url": "sdr1hgn.ddns.net:80", "id": "DL0HGN", "snr": "38", "lat": "53.402192", "lon": "11.218731"}, 238 | {"mac": "985dad7f4aee", "url": "kiwi.minish.org:8073", "id": "IO53hu", "snr": "31", "lat": "53.840000", "lon": "-9.370000"}, 239 | {"mac": "38d2697cbb3d", "url": "kiwisdr4.sdrutah.org:8076", "id": "KK7DV4", "snr": "19", "lat": "41.589151", "lon": "-112.274395"}, 240 | {"mac": "38d269837796", "url": "kiwi.oe9.at:8074", "id": "OE9HLH", "snr": "40", "lat": "47.442516", "lon": "9.843918"}, 241 | {"mac": "e415f6f7741e", "url": "213.109.126.8:8074", "id": "PD0OHW", "snr": "39", "lat": "53.110000", "lon": "6.970000"}, 242 | {"mac": "b0d5cc571610", "url": "sdr.vy.fi:80", "id": "KP13un", "snr": "20", "lat": "63.550000", "lon": "23.710000"}, 243 | {"mac": "e45f01235128", "url": "pso.p.sdrotg.com:80", "id": "KotkaFIN", "snr": "41", "lat": "60.496041", "lon": "26.991192"}, 244 | {"mac": "985dad7f456a", "url": "dl2sba.ddns.net:8073", "id": "DL2SBA", "snr": "21", "lat": "48.670000", "lon": "9.240000"}, 245 | {"mac": "780473833ace", "url": "g4gnk.proxy.kiwisdr.com:8073", "id": "G4GNK", "snr": "26", "lat": "51.818823", "lon": "-0.035258"}, 246 | {"mac": "74e1825d65ae", "url": "73.68.202.112:8073", "id": "WA3ETD", "snr": "25", "lat": "43.680000", "lon": "-73.020000"}, 247 | {"mac": "985dad7f4a82", "url": "gnss.0am.jp:80", "id": "JE1AHJ", "snr": "25", "lat": "35.710307", "lon": "139.495750"}, 248 | {"mac": "38d2698370f8", "url": "ntkiwi.uni-paderborn.de:8073", "id": "DB1UJ", "snr": "12", "lat": "51.709104", "lon": "8.768726"}, 249 | {"mac": "40bd322ecc6d", "url": "88.220.45.111:8073", "id": "SQ5BPD", "snr": "26", "lat": "52.350000", "lon": "20.600000"}, 250 | {"mac": "0035ff8e0580", "url": "kiwisdr.spookhollow.net:8073", "id": "E0UCD", "snr": "20", "lat": "51.211208", "lon": "-0.329840"}, 251 | {"mac": "40bd32e55008", "url": "sdr.hfunderground.com:8079", "id": "W3HFU/1", "snr": "13", "lat": "39.704000", "lon": "-76.974000"}, 252 | {"mac": "04a316dd6657", "url": "kiwisdr.njctech.com:8073", "id": "W1NJC", "snr": "20", "lat": "42.110000", "lon": "-71.710000"}, 253 | {"mac": "b8804f260f54", "url": "n0ls.homedns.org:8073", "id": "N0LS", "snr": "11", "lat": "39.987567", "lon": "-105.033435"}, 254 | {"mac": "28ec9aea49ce", "url": "gibsons.dyndns.org:8073", "id": "G8PFR", "snr": "2", "lat": "51.910000", "lon": "-2.510000"}, 255 | {"mac": "e062346708cf", "url": "m14.fernschreibstelle.de:8073", "id": "DO5EU/JO62qm", "snr": "15", "lat": "52.521490", "lon": "13.383490"}, 256 | {"mac": "40bd32c8d280", "url": "87.251.229.127:8073", "id": "SP1EXB", "snr": "26", "lat": "53.850000", "lon": "16.680000"}, 257 | {"mac": "40bd32e4d57d", "url": "ka7u.no-ip.org:8075", "id": "KA7U", "snr": "0", "lat": "44.245234", "lon": "-117.005488"}, 258 | {"mac": "6433db466280", "url": "kiwisdr-dk9ip.ddns.net:8073", "id": "JN48fx", "snr": "33", "lat": "48.974000", "lon": "8.461950"}, 259 | {"mac": "985dad7f7a85", "url": "83.162.220.82:8073", "id": "PA0RDT", "snr": "15", "lat": "51.500500", "lon": "3.600690"}, 260 | {"mac": "38d2697cd2cd", "url": "kiwisdr1.sdrutah.org:8073", "id": "KA7OEI1", "snr": "25", "lat": "41.590000", "lon": "-112.270000"}, 261 | {"mac": "883f4a9834a8", "url": "kiwisdr.cathalferris.com:8074", "id": "HB9HJF/2", "snr": "8", "lat": "47.400000", "lon": "8.400000"}, 262 | {"mac": "40bd32e5550c", "url": "kiwisdr.cathalferris.com:8073", "id": "HB9HJF/1", "snr": "6", "lat": "47.400000", "lon": "8.400000"}, 263 | {"mac": "780473838586", "url": "kareliamwdx.ddns.net:8073", "id": "KP52cn", "snr": "0", "lat": "62.560000", "lon": "30.180000"}, 264 | {"mac": "1442fc0f8e00", "url": "websdr.uk:8079", "id": "G4SZM/9", "snr": "43", "lat": "51.317266", "lon": "-2.950479"}, 265 | {"mac": "985dad7f0dd5", "url": "emeraldsdr1.proxy.kiwisdr.com:8073", "id": "IO62nu/3", "snr": "6", "lat": "52.870000", "lon": "-6.850000"}, 266 | {"mac": "04a316fcb3ea", "url": "z2abpyweepil7rhq.myfritz.net:8073", "id": "DC9FD/3", "snr": "33", "lat": "50.540000", "lon": "9.470000"}, 267 | {"mac": "38d269837e91", "url": "sdr1.on1aff.be:80", "id": "ON1AFF", "snr": "20", "lat": "51.094345", "lon": "4.514251"}, 268 | {"mac": "b0d5cc571b7b", "url": "62.2.184.6:8073", "id": "HB9AZT", "snr": "32", "lat": "47.410000", "lon": "9.580000"}, 269 | {"mac": "e415f6f96ff0", "url": "59.129.216.68:8073", "id": "JJ1LIB", "snr": "17", "lat": "35.450000", "lon": "139.630000"}, 270 | {"mac": "40bd32e4cfd7", "url": "gti.proxy.kiwisdr.com:8073", "id": "PE0RL", "snr": "31", "lat": "51.670000", "lon": "5.580000"}, 271 | {"mac": "6433db5a6a4c", "url": "122.117.30.20:8073", "id": "BV5AC", "snr": "0", "lat": "23.900000", "lon": "120.520000"}, 272 | {"mac": "e415f6f23445", "url": "websdr.uk:8078", "id": "G4SZM/8", "snr": "41", "lat": "51.317266", "lon": "-2.950479"}, 273 | {"mac": "e415f6f75390", "url": "82.74.101.138:8073", "id": "JO32hu", "snr": "25", "lat": "52.859104", "lon": "6.604887"}, 274 | {"mac": "b0d5ccfc7404", "url": "kiwisdr.ddnss.de:8073", "id": "DL6ECS", "snr": "16", "lat": "50.936075", "lon": "11.586538"}, 275 | {"mac": "e415f6f74da2", "url": "kiwi.web-sdr.at:8073", "id": "OE3KDC", "snr": "22", "lat": "47.801795", "lon": "14.933132"}, 276 | {"mac": "b0d5ccfe3042", "url": "eemedia.mynetgear.com:8073", "id": "FN31do", "snr": "25", "lat": "41.612703", "lon": "-73.691838"}, 277 | {"mac": "e415f6f745cb", "url": "sv1xv.ddns.net:8073", "id": "SV1XV", "snr": "17", "lat": "38.010000", "lon": "23.700000"}, 278 | {"mac": "40bd322ea054", "url": "midtn.dynu.net:8073", "id": "PC4ALP", "snr": "10", "lat": "36.240000", "lon": "-86.640000"}, 279 | {"mac": "74e1828e070c", "url": "kiwi8073.ddns.net:8073", "id": "IV3HZR/3", "snr": "15", "lat": "45.606785", "lon": "13.744692"}, 280 | {"mac": "38d269837eca", "url": "ve6hfd.ddns.net:8074", "id": "VE6HFD", "snr": "17", "lat": "51.630000", "lon": "-111.990000"}, 281 | {"mac": "38d2697cbb0a", "url": "oz1bfm.proxy.kiwisdr.com:8073", "id": "OZ1BFM", "snr": "21", "lat": "56.080000", "lon": "12.160000"}, 282 | {"mac": "6433db38a009", "url": "nndatentechnik.dynv6.net:8073", "id": "DO6NIK", "snr": "39", "lat": "51.734884", "lon": "8.738878"}, 283 | {"mac": "985dad7f0de4", "url": ":8073", "id": "G0EZY/1", "snr": "20", "lat": "53.569677", "lon": "-1.061190"}, 284 | {"mac": "b827ebd2868f", "url": "swiss.ham-radio-op.net:8073", "id": "JN36hn", "snr": "14", "lat": "46.552000", "lon": "6.642000"}, 285 | {"mac": "64cfd9cffb2b", "url": "pa4hjh.ddns.net:8073", "id": "PA4HJH", "snr": "34", "lat": "52.845351", "lon": "5.699363"}, 286 | {"mac": "0c1c57090756", "url": "sk5sm.dyndns.info:8073", "id": "SK5SM", "snr": "36", "lat": "58.537633", "lon": "14.443303"}, 287 | {"mac": "e45f010ec061", "url": "hb9ttu.dyndns.org:8075", "id": "HB9TTU", "snr": "11", "lat": "47.385085", "lon": "8.781054"}, 288 | {"mac": "6433db174cbe", "url": "oh5ae.dyndns.org:8073", "id": "OH5AE", "snr": "35", "lat": "60.720000", "lon": "26.410000"}, 289 | {"mac": "dca6327d7c73", "url": "sergiocorda.synology.me:58075", "id": "JM49nf", "snr": "17", "lat": "39.220000", "lon": "9.130000"}, 290 | {"mac": "c4f3127256da", "url": "norfolk.george-smart.co.uk:8073", "id": "M1GEO", "snr": "25", "lat": "52.441319", "lon": "1.214225"}, 291 | {"mac": "c4f312a6844e", "url": "shack.oe9.at:8073", "id": "JN47um", "snr": "28", "lat": "47.500000", "lon": "9.720000"}, 292 | {"mac": "dca6326d3b71", "url": "admtan.p.sdrotg.com:80", "id": "PM94nw", "snr": "17", "lat": "34.956000", "lon": "139.092000"}, 293 | {"mac": "84eb18e54e3a", "url": "119.47.17.25:8073", "id": "JA5FP", "snr": "16", "lat": "35.667916", "lon": "140.171996"}, 294 | {"mac": "985dad7f4aa9", "url": "ce3pbr.proxy.kiwisdr.com:8073", "id": "CA3PBR", "snr": "12", "lat": "-33.521667", "lon": "-70.568694"}, 295 | {"mac": "c4f312a093ed", "url": "81.168.1.206:8073", "id": "G3XOU", "snr": "32", "lat": "50.530000", "lon": "-4.130000"}, 296 | {"mac": "883f4aa8d915", "url": "kiwibsb.ddns.net:8074", "id": "PT2FHC/4", "snr": "24", "lat": "-15.710000", "lon": "-47.820000"}, 297 | {"mac": "c4f312a678f3", "url": "swiss.ham-radio-op.net:8074", "id": "HE9JAP/4", "snr": "15", "lat": "46.545000", "lon": "6.627000"}, 298 | {"mac": "40bd32e2d9ab", "url": "86.135.190.85:8073", "id": "MX0NCA", "snr": "18", "lat": "52.933400", "lon": "1.272500"}, 299 | {"mac": "985dad876650", "url": "izh.swl.su:80", "id": "LO66ou", "snr": "11", "lat": "56.836560", "lon": "53.227097"}, 300 | {"mac": "28ec9af32af4", "url": "erserver.ddns.net:8073", "id": "JO50uw", "snr": "21", "lat": "50.930000", "lon": "11.680000"}, 301 | {"mac": "884aeaf59046", "url": "kiwisdr-dm7rm.goip.de:8073", "id": "DM7RM", "snr": "29", "lat": "49.523685", "lon": "9.313056"}, 302 | {"mac": "883f4aa8cf34", "url": "sdr.k9mq.com:8073", "id": "K9MQ", "snr": "10", "lat": "41.450000", "lon": "-87.440000"}, 303 | {"mac": "38d2697cd800", "url": "82.70.254.222:8073", "id": "IO82qv", "snr": "0", "lat": "52.890000", "lon": "-2.640000"}, 304 | {"mac": "28ec9aeff774", "url": "jimlill.com:8075", "id": "WA2ZKD", "snr": "22", "lat": "43.126741", "lon": "-77.584850"}, 305 | {"mac": "e415f6f502a4", "url": "sdr.hfunderground.com:8075", "id": "W3HFU/3", "snr": "19", "lat": "39.704000", "lon": "-76.974000"}, 306 | {"mac": "6433db38da8f", "url": "ve6lrn.narc.net:8073", "id": "VE6LRN", "snr": "18", "lat": "53.450000", "lon": "-114.540000"}, 307 | {"mac": "0c1c57099f75", "url": "hik.iptime.org:8075", "id": "HL3AMO", "snr": "9", "lat": "36.387000", "lon": "127.321000"}, 308 | {"mac": "9884e3aeda64", "url": "nsk.swl.su:80", "id": "NO14th", "snr": "27", "lat": "54.300000", "lon": "83.630000"}, 309 | {"mac": "38d2697ce5ac", "url": "186.193.231.135:8073", "id": "GG66rf", "snr": "12", "lat": "-23.770058", "lon": "-46.535755"}, 310 | {"mac": "40bd32e4cc5f", "url": "88.163.155.169:18073", "id": "Sakamurro", "snr": "19", "lat": "46.290000", "lon": "6.090000"}, 311 | {"mac": "985dad48a547", "url": "julussdalen.proxy.kiwisdr.com:8073", "id": "JP51ua", "snr": "27", "lat": "61.037636", "lon": "11.689167"}, 312 | {"mac": "fc69474a3914", "url": "sdr.vebik.cz:8073", "id": "OK1KKI", "snr": "41", "lat": "49.142938", "lon": "15.006931"}, 313 | {"mac": "1442fc0f60ab", "url": "ik7fmo.ddns.net:8073", "id": "IK7FMO", "snr": "27", "lat": "40.970000", "lon": "17.120000"}, 314 | {"mac": "6433db48f061", "url": "kiwisdr.servepics.com:8073", "id": "JO52ue", "snr": "23", "lat": "52.180200", "lon": "11.732500"}, 315 | {"mac": "c4f312b9ff30", "url": "kb6c.proxy.kiwisdr.com:8073", "id": "KB6C", "snr": "22", "lat": "34.750000", "lon": "-119.080000"}, 316 | {"mac": "6433db1c02dc", "url": "178.63.122.223:8073", "id": "JO41tb", "snr": "13", "lat": "51.075500", "lon": "9.615500"}, 317 | {"mac": "c4f312756f70", "url": "kiwi.dd9lh.de:8073", "id": "DD9LH", "snr": "16", "lat": "54.200000", "lon": "9.090000"}, 318 | {"mac": "985dad38006d", "url": "la2g.ddns.net:8072", "id": "LA2G", "snr": "37", "lat": "60.147410", "lon": "11.146040"}, 319 | {"mac": "883f4aa8c5e3", "url": "24.78.155.2:8073", "id": "W6LPV", "snr": "14", "lat": "49.860000", "lon": "-97.420000"}, 320 | {"mac": "40bd32e54015", "url": "Hallmann.selfhost.eu:80", "id": "JN49av", "snr": "24", "lat": "49.890000", "lon": "8.070000"}, 321 | {"mac": "40bd32c8d492", "url": "kiwisdr.k3fef.com:8073", "id": "K3FEF", "snr": "26", "lat": "41.325953", "lon": "-74.922089"}, 322 | {"mac": "04a316edf709", "url": "kiwisdr.ve6slp.ca:8173", "id": "VE6JY1", "snr": "18", "lat": "53.742089", "lon": "-112.830264"}, 323 | {"mac": "5051a9a7944e", "url": "jimlill.com:8076", "id": "WA2ZKD/6", "snr": "0", "lat": "43.126741", "lon": "-77.584850"}, 324 | {"mac": "c4f312726fdf", "url": "oe3akb.ddns.net:8073", "id": "OE3AKB", "snr": "35", "lat": "48.320000", "lon": "15.580000"}, 325 | {"mac": "0cb2b7d6cd86", "url": "83.247.88.41:8073", "id": "JO21nn", "snr": "16", "lat": "51.560000", "lon": "5.100000"}, 326 | {"mac": "884aeaf5408e", "url": "kiwisdr.owdjim.gen.nz:8073", "id": "Marahau", "snr": "8", "lat": "-41.006161", "lon": "173.013660"}, 327 | {"mac": "80f5b5f56892", "url": "sdr.zapto.org:8073", "id": "ZL2QF/RF70fk", "snr": "15", "lat": "-39.574552", "lon": "174.434932"}, 328 | {"mac": "40bd32c8d810", "url": "kiwi.ziegler.bz:8073", "id": "HB9GVC", "snr": "14", "lat": "47.365368", "lon": "8.897944"}, 329 | {"mac": "985dad48e186", "url": "kiwisdr.tecsunradios.com.au:8073", "id": "Tecsun", "snr": "13", "lat": "-35.632626", "lon": "149.807140"}, 330 | {"mac": "38d2697cc27d", "url": "vk2aak.ddns.net:8073", "id": "VK2AAK", "snr": "13", "lat": "-32.080000", "lon": "152.480000"}, 331 | {"mac": "884aeaf5907c", "url": "kiwisdr.k1ra.us:8073", "id": "K1RA", "snr": "20", "lat": "38.742405", "lon": "-77.797874"}, 332 | {"mac": "e0623467033b", "url": "hb9efk.proxy.kiwisdr.com:8073", "id": "HB9EFK", "snr": "11", "lat": "47.267764", "lon": "8.314324"}, 333 | {"mac": "78047338fe98", "url": "sdr-bayern.spdns.de:8073", "id": "JN69fg", "snr": "39", "lat": "49.290000", "lon": "12.440000"}, 334 | {"mac": "544a16e70e44", "url": "balou.spdns.de:8074", "id": "JO64pb", "snr": "21", "lat": "54.080000", "lon": "13.280000"}, 335 | {"mac": "7c386655e729", "url": "msk.swl.su:8073", "id": "KO85qw", "snr": "17", "lat": "55.920000", "lon": "37.400000"}, 336 | {"mac": "40bd32e4da2a", "url": "irelandnorthwest.proxy.kiwisdr.com:8073", "id": "NorthWest", "snr": "44", "lat": "54.229144", "lon": "-9.312494"}, 337 | {"mac": "883f4aabca66", "url": "sdr.hfunderground.com:8076", "id": "W3HFU/2", "snr": "25", "lat": "39.704000", "lon": "-76.974000"}, 338 | {"mac": "e062346fdfb3", "url": "jpvm54.ddns.net:8073", "id": "KA0GBG", "snr": "8", "lat": "39.680000", "lon": "-105.120000"}, 339 | {"mac": "c4f312727699", "url": "78.40.253.65:80", "id": "TF3GZ", "snr": "5", "lat": "66.450000", "lon": "-15.950000"}, 340 | {"mac": "6433db175099", "url": "nikola.animats.net:8073", "id": "VK2KTJ", "snr": "2", "lat": "-33.640000", "lon": "150.280000"}, 341 | {"mac": "0cb2b7d6efd6", "url": "sigmasdr.ddns.net:8074", "id": "N1NTE/2", "snr": "25", "lat": "42.033943", "lon": "-72.140461"}, 342 | {"mac": "883f4aa8acf6", "url": "69.27.184.58:8075", "id": "KFSSW", "snr": "10", "lat": "37.382406", "lon": "-122.413786"}, 343 | {"mac": "04a316eda0a3", "url": "la1d.proxy.kiwisdr.com:8073", "id": "LA1D", "snr": "40", "lat": "60.950870", "lon": "11.295140"}, 344 | {"mac": "1442fc0f98c4", "url": "vilradio.dynv6.net:8073", "id": "DL1NDG", "snr": "21", "lat": "49.455160", "lon": "11.079390"}, 345 | {"mac": "78047338fece", "url": "67.233.122.90:8073", "id": "w4kel", "snr": "23", "lat": "38.150000", "lon": "-78.560000"}, 346 | {"mac": "3403de656ece", "url": "kiwisdr.on3rvh.be:8073", "id": "ON3RVH", "snr": "24", "lat": "51.249976", "lon": "2.998107"}, 347 | {"mac": "3403de637ca1", "url": "81.174.134.21:8073", "id": "G3PWJ", "snr": "20", "lat": "52.445614", "lon": "-2.129931"}, 348 | {"mac": "e415f6f6d4d0", "url": "202.127.177.27:8074", "id": "PM96vn", "snr": "24", "lat": "36.550000", "lon": "139.800000"}, 349 | {"mac": "883f4aabaff6", "url": "69.27.184.58:8074", "id": "KFSNW", "snr": "15", "lat": "37.384275", "lon": "-122.413133"}, 350 | {"mac": "2476257e1bc4", "url": "109.194.11.214:8073", "id": "R3YBN", "snr": "15", "lat": "53.300000", "lon": "34.250000"}, 351 | {"mac": "38d2697ccb40", "url": "jp7fso.proxy.kiwisdr.com:8073", "id": "JP7FSO", "snr": "24", "lat": "37.669419", "lon": "140.491418"}, 352 | {"mac": "e415f6f80bc2", "url": "192.168.1.148:8073", "id": "E0MAH", "snr": "2", "lat": "53.610000", "lon": "-1.360000"}, 353 | {"mac": "c4f312753037", "url": "sdr-amradioantennas.com:8074", "id": "VK3KHZ/4", "snr": "12", "lat": "-37.817000", "lon": "145.288000"}, 354 | {"mac": "e062346fd635", "url": "rvrgm.asuscomm.com:8073", "id": "OF77vq", "snr": "8", "lat": "-32.330000", "lon": "115.820000"}, 355 | {"mac": "78047347a2bc", "url": "n4dkd.asuscomm.com:8901", "id": "N4DKD", "snr": "11", "lat": "33.380000", "lon": "-86.720000"}, 356 | {"mac": "40bd32e4d538", "url": "n0emp.ddns.net:8073", "id": "N0EMP", "snr": "21", "lat": "40.480000", "lon": "-105.070000"}, 357 | {"mac": "40bd32e93a4d", "url": "85.183.11.108:8073", "id": "DF3LZ", "snr": "32", "lat": "53.450757", "lon": "10.220651"}, 358 | {"mac": "1442fc0f74f4", "url": "9k2ra-4.proxy.kiwisdr.com:8073", "id": "LL39xi/3", "snr": "26", "lat": "29.364500", "lon": "47.988899"}, 359 | {"mac": "5051a9987dbc", "url": "sdr.hfunderground.com:8074", "id": "W3HFU", "snr": "23", "lat": "39.704000", "lon": "-76.974000"}, 360 | {"mac": "0035ff9cf993", "url": "g4fui.ddns.net:8073", "id": "G4FUI/IO84pp", "snr": "17", "lat": "54.670000", "lon": "-2.730000"}, 361 | {"mac": "985dad493529", "url": "sdr.tambov.gq:8901", "id": "LO02ss", "snr": "0", "lat": "52.763229", "lon": "41.502566"}, 362 | {"mac": "0cb2b7d6da9a", "url": "kb1.psokiwi.net:8073", "id": "SDXLKB1", "snr": "0", "lat": "60.587472", "lon": "26.448102"}, 363 | {"mac": "689e198fab29", "url": "sergiocorda.synology.me:58073", "id": "JM49nf/3", "snr": "20", "lat": "39.220000", "lon": "9.130000"}, 364 | {"mac": "0035ff8df93e", "url": "kiwisdr.dynpc.net:8073", "id": "DO1TI", "snr": "27", "lat": "53.430000", "lon": "9.770000"}, 365 | {"mac": "985dad7f14e8", "url": "n1bpd.net:8073", "id": "N1BPD", "snr": "18", "lat": "41.703720", "lon": "-70.155635"}, 366 | {"mac": "883f4a8c7e84", "url": "hl3amo.ddns.net:8074", "id": "HL3AMO/4", "snr": "16", "lat": "36.387000", "lon": "127.321000"}, 367 | {"mac": "6433db1fbc61", "url": "tustinfarm.ddns.net:2311", "id": "KX4AZ", "snr": "11", "lat": "44.114150", "lon": "-85.459500"}, 368 | {"mac": "e415f6f487e9", "url": "sk6ag1.ddns.net:8071", "id": "JO67nl", "snr": "29", "lat": "57.493215", "lon": "13.097758"}, 369 | {"mac": "9884e3938a46", "url": "winsenallerkiwi.ddns.net:8076", "id": "WinsenAller", "snr": "3", "lat": "52.673055", "lon": "9.871644"}, 370 | {"mac": "78047338ad5c", "url": "69.27.184.58:8073", "id": "KFSOmni", "snr": "15", "lat": "37.390000", "lon": "-122.410000"}, 371 | {"mac": "985dad382ef0", "url": "141.135.55.42:8073", "id": "ON7AVC", "snr": "22", "lat": "50.8549541", "lon": "4.3053505"}, 372 | {"mac": "985dad7f80cb", "url": "sdrbris.proxy.kiwisdr.com:8073", "id": "QG62ms", "snr": "12", "lat": "-27.220000", "lon": "153.010000"}, 373 | {"mac": "883f4aab0c13", "url": "69.27.184.58:8076", "id": "KFSSE", "snr": "12", "lat": "37.383136", "lon": "-122.413261"}, 374 | {"mac": "74e182a7b32a", "url": "alfaromeo170.myqnapcloud.com:8073", "id": "JO61al", "snr": "8", "lat": "51.480518", "lon": "12.033653"}, 375 | {"mac": "0cb2b7d6efbe", "url": "km6cq.hopto.org:8073", "id": "KM6CQ", "snr": "13", "lat": "39.310000", "lon": "-119.790000"}, 376 | {"mac": "04a316bd7f88", "url": "kiwisdr.ku4by.com:8073", "id": "KU4BY", "snr": "23", "lat": "36.250000", "lon": "-76.220000"}, 377 | {"mac": "883f4a9cc9a5", "url": "nw8s-kiwi.ddns.net:8073", "id": "NW8S", "snr": "26", "lat": "41.404660", "lon": "-82.116985"}, 378 | {"mac": "e415f6f74439", "url": "213.109.126.8:8073", "id": "PD0OHW/3", "snr": "40", "lat": "53.110000", "lon": "6.970000"}, 379 | {"mac": "6433db4655cb", "url": "80.56.195.123:8073", "id": "JO22jl", "snr": "2", "lat": "52.474033", "lon": "4.809525"}, 380 | {"mac": "fc6947b8273d", "url": "sk6ag2.ddns.net:8072", "id": "SK6AG", "snr": "28", "lat": "57.720000", "lon": "13.070000"}, 381 | {"mac": "40bd32c8d063", "url": "s57bit.proxy.kiwisdr.com:8073", "id": "S57BIT", "snr": "19", "lat": "46.170000", "lon": "14.300000"}, 382 | {"mac": "40bd32e55056", "url": "67.170.2.228:8086", "id": "WA7HL", "snr": "12", "lat": "48.760000", "lon": "-122.510000"}, 383 | {"mac": "e415f6f74ae1", "url": "birdsnest.zapto.org:8073", "id": "JO32qk", "snr": "22", "lat": "52.430000", "lon": "7.340000"}, 384 | {"mac": "40bd32e518e2", "url": "sdr.hfunderground.com:8073", "id": "W3HFU/1/3", "snr": "27", "lat": "39.704000", "lon": "-76.974000"}, 385 | {"mac": "884aeaf58c0f", "url": "s4wmv4hovyls5ebz.myfritz.net:8073", "id": "SWLJO43/1", "snr": "37", "lat": "53.310000", "lon": "9.990000"}, 386 | {"mac": "780473836210", "url": "w7pua-2.ddns.net:8073", "id": "W7PUA", "snr": "17", "lat": "44.676700", "lon": "-123.227333"}, 387 | {"mac": "b8804f25cd5a", "url": "kiwisdr-bern.try.yaler.io:80", "id": "JN36qw", "snr": "37", "lat": "46.955462", "lon": "7.367836"}, 388 | {"mac": "883f4aa8cf2b", "url": "home.jeffreyrandow.org:8075", "id": "N5SNT", "snr": "12", "lat": "29.672000", "lon": "-98.116000"}, 389 | {"mac": "40bd32c8d498", "url": "kiwistbg.ddns.net:8074", "id": "HB9TTU/4", "snr": "15", "lat": "47.385100", "lon": "8.915500"}, 390 | {"mac": "5051a998d422", "url": "jimlill.com:8073", "id": "WA2ZKD/3", "snr": "11", "lat": "43.126741", "lon": "-77.584850"}, 391 | {"mac": "b0d5cc552cf3", "url": "202.127.177.27:8073", "id": "PM96vn/3", "snr": "22", "lat": "36.550000", "lon": "139.800000"}, 392 | {"mac": "1442fc0f6654", "url": "83.128.85.68:8073", "id": "PI4AMF/JO22q", "snr": "34", "lat": "52.240202", "lon": "5.429781"}, 393 | {"mac": "b827eb5ebc99", "url": "tredxk.no-ip.org:8073", "id": "KP11wm/3", "snr": "27", "lat": "61.528000", "lon": "23.902000"}, 394 | {"mac": "38d269837ec7", "url": "boavista.twrmon.net:8073", "id": "TWR/BoaVista", "snr": "18", "lat": "2.820000", "lon": "-60.670000"}, 395 | {"mac": "40bd32e4c8b1", "url": "dl4qb.proxy.kiwisdr.com:8073", "id": "DL4QB", "snr": "30", "lat": "51.760000", "lon": "7.390000"}, 396 | {"mac": "0035ff98e239", "url": "59.167.177.246:8073", "id": "VK3JTM", "snr": "15", "lat": "-37.290743", "lon": "142.939774"}, 397 | {"mac": "985dad51600d", "url": "sdr.m0taz.co.uk:8073", "id": "M0TAZ", "snr": "21", "lat": "51.583729", "lon": "0.199700"}, 398 | {"mac": "40bd32c8d6b4", "url": "beefy.fun:8073", "id": "FN42nl", "snr": "22", "lat": "42.489110", "lon": "-70.904797"}, 399 | {"mac": "1442fc0f60c9", "url": "kiwisdr1.hoka.co.uk:8072", "id": "STNB/2", "snr": "20", "lat": "51.292581", "lon": "0.979165"}, 400 | {"mac": "b827ebed9a7e", "url": "kb1vwc.ddns.net:8073", "id": "KB1VWC", "snr": "23", "lat": "41.570000", "lon": "-70.550000"}, 401 | {"mac": "38d2697cbb58", "url": "78.71.162.126:8073", "id": "JO57uq", "snr": "30", "lat": "57.691322", "lon": "11.684921"}, 402 | {"mac": "9884e38cd8b8", "url": "vk6qs.proxy.kiwisdr.com:8073", "id": "VK6QS", "snr": "32", "lat": "-32.360000", "lon": "116.620000"}, 403 | {"mac": "c4f312b9fac5", "url": "12.29.214.134:8073", "id": "WO7I", "snr": "24", "lat": "40.920000", "lon": "-117.760000"}, 404 | {"mac": "6433db4655b3", "url": "kiwisdr.ve7av.ca:8073", "id": "VE7AV", "snr": "16", "lat": "53.810000", "lon": "-122.830000"}, 405 | {"mac": "780473832538", "url": "k1fb.proxy.kiwisdr.com:8073", "id": "EM66mf", "snr": "10", "lat": "36.240000", "lon": "-86.920000"}, 406 | {"mac": "40bd32e53ceb", "url": "kk6pr.ddns.net:8075", "id": "KK6PR", "snr": "16", "lat": "44.426300", "lon": "-121.262300"}, 407 | {"mac": "60640538d7b9", "url": "kphsdr.com:8073", "id": "CM88mc/3", "snr": "14", "lat": "38.100000", "lon": "-122.950000"}, 408 | {"mac": "6433db465592", "url": "kiwi.dol.la:8073", "id": "ScratchMoney", "snr": "14", "lat": "30.109691", "lon": "-92.124861"}, 409 | {"mac": "38d2697cbb55", "url": "sdr-amradioantennas.com:8071", "id": "VK3KHZ/1", "snr": "12", "lat": "-37.817000", "lon": "145.288000"}, 410 | {"mac": "883f4aa8b65c", "url": "2gbl9qiptwfhej62.myfritz.net:8073", "id": "JO62rb", "snr": "23", "lat": "52.050000", "lon": "13.430000"}, 411 | {"mac": "1442fc0f74ee", "url": "84.3.151.98:8073", "id": "HA2NA", "snr": "30", "lat": "47.810110", "lon": "18.747200"}, 412 | {"mac": "780473393a41", "url": "kiwi.ve7gl.ca:8073", "id": "VE7GL", "snr": "16", "lat": "49.090000", "lon": "-122.010000"}, 413 | {"mac": "883f4aa8cf1c", "url": "lu4eec.ddns.net:8073", "id": "LU4EEC", "snr": "11", "lat": "-34.656060", "lon": "-58.541084"}, 414 | {"mac": "40bd322ea01e", "url": "209.115.233.71:8074", "id": "ve6ars2", "snr": "15", "lat": "50.080000", "lon": "-113.680000"}, 415 | {"mac": "40bd32c8d2d0", "url": "kiwiradio.proxy.kiwisdr.com:8073", "id": "JN97tf", "snr": "31", "lat": "47.240000", "lon": "19.610000"}, 416 | {"mac": "b8804f25c900", "url": "boezberg600m.ddns.net:8073", "id": "JN47bl", "snr": "11", "lat": "47.499476", "lon": "8.154871"}, 417 | {"mac": "04a316ee0fa3", "url": "hg5acz.ddns.net:8073", "id": "HG5ACZ", "snr": "22", "lat": "47.691040", "lon": "17.253259"}, 418 | {"mac": "247d4d1b6834", "url": "jimlill.com:8074", "id": "WA2ZKD/4", "snr": "0", "lat": "43.126741", "lon": "-77.584850"}, 419 | {"mac": "d0ff50091fd3", "url": "hfrx.ddns.net:8073", "id": "XE2MCC", "snr": "6", "lat": "24.029643", "lon": "-104.660274"}, 420 | {"mac": "6433db1bde10", "url": "kiwi.dl7vdx.com:8073", "id": "DL7VDX", "snr": "39", "lat": "52.658220", "lon": "13.781170"}, 421 | {"mac": "b8804f25c910", "url": "shtsf.ddns.net:8073", "id": "F6KOH", "snr": "18", "lat": "49.508367", "lon": "0.162412"}, 422 | {"mac": "40bd32c8d48f", "url": "80.75.112.98:8074", "id": "HB9NF/4", "snr": "11", "lat": "47.328590", "lon": "8.961670"}, 423 | {"mac": "985dad7f5e27", "url": "lu4aa.proxy.kiwisdr.com:8073", "id": "LU5AGQ", "snr": "22", "lat": "-34.866789", "lon": "-58.139648"}, 424 | {"mac": "e415f6f785fd", "url": "sdxlkiwi7.proxy.kiwisdr.com:8073", "id": "KP22ag", "snr": "18", "lat": "62.265653", "lon": "24.032185"}, 425 | {"mac": "40bd32e9420f", "url": "kiwi.narodnaya.keenetic.link:80", "id": "KO37xu", "snr": "29", "lat": "57.855869", "lon": "27.950980"}, 426 | {"mac": "04a316fd98d4", "url": "hb9exc.iotvs.ch:8073", "id": "HB9EXC", "snr": "25", "lat": "46.135100", "lon": "7.116000"} 427 | ] -------------------------------------------------------------------------------- /compute_ultimate.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | """ compute_ultimate python code. """ 4 | 5 | # python 2/3 compatibility 6 | from __future__ import print_function 7 | from __future__ import division 8 | from __future__ import absolute_import 9 | 10 | import glob 11 | import sys 12 | import json 13 | import os 14 | from os.path import dirname as up 15 | import re 16 | import signal 17 | import subprocess 18 | import threading 19 | import time 20 | import math 21 | import platform 22 | import webbrowser 23 | from io import BytesIO 24 | from collections import OrderedDict 25 | import numpy as np 26 | import matplotlib.pyplot as plt 27 | from matplotlib.colors import LinearSegmentedColormap 28 | from PIL import Image, ImageTk 29 | 30 | # python 2/3 compatibility 31 | if sys.version_info[0] == 2: 32 | import tkMessageBox 33 | from Tkinter import Checkbutton, CURRENT, IntVar, Listbox 34 | from Tkinter import Entry, Text, Menu, Label, Button, Frame, Tk, PhotoImage, Canvas 35 | from tkFont import Font 36 | 37 | else: 38 | import tkinter.messagebox as tkMessageBox 39 | from tkinter import Checkbutton, CURRENT, IntVar, Listbox 40 | from tkinter import Entry, Text, Menu, Label, Button, Frame, Tk, PhotoImage, Canvas 41 | from tkinter.font import Font 42 | 43 | VERSION = "ultimateTDoA interface v2.10 " 44 | for mfi_le in glob.glob('*.*m*'): 45 | ff = open(mfi_le, 'r') 46 | fi_le_d = ff.read() 47 | ff.close() 48 | FREQUENCY = re.search(r".+title.+\'(.+)\s-\sR.+\'", fi_le_d).group(1) 49 | ALEID = "" 50 | CLICKEDNODE = "" 51 | if str(os.getcwd().rsplit("_", 2)[1]) == "UA": 52 | for ale_file in glob.glob(os.getcwd() + os.sep + "ALE*.txt"): 53 | a = open(ale_file, 'r') 54 | aledata = a.read() 55 | a.close() 56 | try: 57 | ALEID = " [ALE ID: " + str(re.search(r".+\[(TWS|TIS)\]\[(.*)\]\[", aledata).group(2)) + "]" 58 | except AttributeError: 59 | ALEID = " [ALE]" 60 | 61 | cdict1 = { 62 | 'red': ((0.0, 0.0, 0.0), 63 | (0.077, 0.0, 0.0), 64 | (0.16, 0.0, 0.0), 65 | (0.265, 1.0, 1.0), 66 | (0.403, 1.0, 1.0), 67 | (0.604, 1.0, 1.0), 68 | (1.0, 1.0, 1.0)), 69 | 70 | 'green': ((0.0, 0.0, 0.0), 71 | (0.077, 0.0, 0.0), 72 | (0.16, 1.0, 1.0), 73 | (0.265, 1.0, 1.0), 74 | (0.403, 0.0, 0.0), 75 | (0.604, 0.0, 0.0), 76 | (1.0, 0.764, 0.764)), 77 | 78 | 'blue': ((0.0, 0.117, 0.117), 79 | (0.077, 1.0, 1.0), 80 | (0.16, 1.0, 1.0), 81 | (0.265, 0.0, 0.0), 82 | (0.403, 0.0, 0.0), 83 | (0.604, 1.0, 1.0), 84 | (1.0, 1.0, 1.0)), 85 | } 86 | 87 | COLORMAP = LinearSegmentedColormap('SAColorMap', cdict1, 1024) 88 | 89 | 90 | class Restart(object): 91 | """ GUI Restart routine. """ 92 | 93 | def __init__(self): 94 | pass 95 | 96 | def __str__(self): 97 | return self.__class__.__name__ 98 | 99 | @staticmethod 100 | def run(): 101 | """ GUI Restart routine. """ 102 | try: # to kill octave-cli process if exists 103 | os.kill(PROC_PID, signal.SIGTERM) 104 | except (NameError, OSError): 105 | pass 106 | if platform.system() == "Windows": 107 | os.execlp("pythonw.exe", "pythonw.exe", "compute_ultimate.py") 108 | else: 109 | os.execv(sys.executable, [sys.executable] + sys.argv) 110 | 111 | 112 | class ReadKnownPointFile(threading.Thread): 113 | """ Read known location list routine (see directTDoA_knownpoints.db file). """ 114 | 115 | def __init__(self): 116 | super(ReadKnownPointFile, self).__init__() 117 | 118 | def run(self): 119 | """ Read known location list routine (see directTDoA_knownpoints.db file). """ 120 | with open(up(up(up(os.getcwd()))) + os.sep + "directTDoA_knownpoints.db") as h: 121 | global my_info1, my_info2, my_info3 122 | i = 3 # skip the 3x comment lines at start of the text file database 123 | lines = h.readlines() 124 | my_info1 = [] 125 | my_info2 = [] 126 | my_info3 = [] 127 | while i < sum(1 for _ in open(up(up(up(os.getcwd()))) + os.sep + "directTDoA_knownpoints.db")): 128 | inforegexp = re.search(r"(.*),(.*),(.*)", lines[i]) 129 | my_info1.append(inforegexp.group(1)) 130 | my_info2.append(inforegexp.group(2)) 131 | my_info3.append(inforegexp.group(3)) 132 | i += 1 133 | h.close() 134 | 135 | 136 | class ReadCfg(object): 137 | """ DirectKiwi configuration file read process. """ 138 | 139 | def __init__(self): 140 | pass 141 | 142 | @staticmethod 143 | def read_cfg(): 144 | """ compute_ultimate.py configuration file read process. """ 145 | global CFG, DX0, DY0, DX1, DY1, DMAP 146 | global POICOLOR, ICONSIZE, ICONTYPE, HIGHLIGHT 147 | global BGC, FGC, CONS_B, CONS_F, MAP_BOX, GUI 148 | try: 149 | # Read the config file v5.0 format and declare variables 150 | with open(up(up(up(os.getcwd()))) + os.sep + 'directTDoA.cfg', 'r') as config_file: 151 | CFG = json.load(config_file, object_pairs_hook=OrderedDict) 152 | DX0, DX1 = CFG["map"]["x0"], CFG["map"]["x1"] 153 | DY0, DY1 = CFG["map"]["y0"], CFG["map"]["y1"] 154 | DMAP, ICONSIZE = CFG["map"]["file"], CFG["map"]["iconsize"] 155 | POICOLOR, ICONTYPE = CFG["map"]["poi"], CFG["map"]["icontype"] 156 | HIGHLIGHT = CFG["map"]["hlt"] 157 | BGC, FGC = CFG["guicolors"]["main_b"], CFG["guicolors"]["main_f"] 158 | CONS_B, CONS_F = CFG["guicolors"]["cons_b"], CFG["guicolors"]["cons_f"] 159 | MAP_BOX, GUI = CFG["map"]["mapbox"], CFG["map"]["gui"] 160 | except (ImportError, ValueError): 161 | sys.exit("config file not found") 162 | 163 | 164 | class TrimIQ(threading.Thread): 165 | """ trim_iq.py processing routine """ 166 | 167 | def __init__(self, tdoa_rootdir): 168 | super(TrimIQ, self).__init__() 169 | self.tdoa_rootdir = tdoa_rootdir 170 | 171 | def run(self): 172 | APP.gui.writelog("trim_iq.py script started.") 173 | APP.gui.writelog("Click & drag : Select the time portion of the IQ record that you want to keep.") 174 | APP.gui.writelog("Close window : No change made to the IQ.") 175 | APP.gui.writelog("Single click : Deletes IQ.") 176 | APP.gui.writelog("< 2 seconds : Deletes IQ.") 177 | subprocess.call([sys.executable, 'trim_iq.py'], cwd=self.tdoa_rootdir, shell=False) 178 | Restart().run() 179 | 180 | 181 | class PlotIQ(threading.Thread): 182 | """ Plot_iq, enhanced version with SNR ranking. """ 183 | 184 | def __init__(self, iqfile, mode, filelist): 185 | super(PlotIQ, self).__init__() 186 | self.iqfile = iqfile 187 | self.plot_mode = mode 188 | self.filelist = filelist 189 | 190 | def run(self): 191 | global scores 192 | plt.rcParams.update({'figure.max_open_warning': 0}) 193 | # mode 0 = GUI display preview 194 | # mode 1 = temp.pdf (for a run with 3 < nodes < 6) 195 | if self.plot_mode == 1: 196 | files = {path: self.load(path) for path in self.filelist} 197 | scores = sorted([(path, self.score(data)) for path, data in files.items()], key=lambda item: item[1], 198 | reverse=True) 199 | self.plot(files, scores, cols=3, iqfile=files) 200 | else: 201 | self.plot(self.load(self.iqfile), order=None, cols=1, iqfile=self.iqfile) 202 | 203 | @staticmethod 204 | def load(path): 205 | """ Remove GNSS from IQ recordings. """ 206 | buf = BytesIO() 207 | with open(path, 'rb') as f: 208 | size = os.path.getsize(path) 209 | for i in range(62, size, 2074): 210 | f.seek(i) 211 | buf.write(f.read(2048)) 212 | data = np.frombuffer(buf.getvalue(), dtype='int16') 213 | data = data[0::2] + 1j * data[1::2] 214 | return data 215 | 216 | @staticmethod 217 | def score(data): 218 | """ IQ SNR calculation. """ 219 | max_snr = 0.0 220 | for offset in range(12000, len(data), 512): 221 | snr = np.std(np.fft.fft(data[offset:], n=1024)) 222 | if snr > max_snr: 223 | max_snr = snr 224 | return max_snr 225 | 226 | @staticmethod 227 | def has_gps(path): 228 | """ Detect if IQ file has GPS GNSS data (in test). """ 229 | gpslast = 0 230 | f_wav = open(path, 'rb') 231 | for i in range(2118, os.path.getsize(path), 2074): 232 | f_wav.seek(i) 233 | if sys.version_info[0] < 3: 234 | gpslast = max(gpslast, ord(f_wav.read(1)[0])) 235 | else: 236 | gpslast = max(gpslast, f_wav.read(1)[0]) 237 | return 0 < gpslast < 254 238 | 239 | @staticmethod 240 | def plot(files, order, cols, iqfile): 241 | if not order: 242 | # mode 0 243 | global CLICKEDNODE 244 | CLICKEDNODE = iqfile.rsplit('_', 3)[2] 245 | buf = BytesIO() 246 | fig, a_x = plt.subplots() 247 | a_x.specgram(files, NFFT=1024, Fs=12000, window=lambda data: data * np.hanning(len(data)), noverlap=512, 248 | vmin=10, vmax=200, cmap=COLORMAP) 249 | a_x.set_title(iqfile.rsplit('_', 3)[2]) 250 | a_x.axes.get_yaxis().set_visible(False) 251 | plt.savefig(buf, bbox_inches='tight') 252 | img = ImageTk.PhotoImage(Image.open(buf).resize((320, 240), Image.ANTIALIAS)) 253 | APP.gui.plot_iq_button.configure(image=img) 254 | APP.gui.plot_iq_button.image = img 255 | else: 256 | # mode 1 257 | rows = int(math.ceil(len(files) / cols)) 258 | fig, axs = plt.subplots(ncols=cols, nrows=rows) 259 | fig.set_figwidth(cols * 5.27) 260 | fig.set_figheight(rows * 3) 261 | for i, (path, _) in enumerate(order): 262 | a_x = axs.flat[i] 263 | a_x.specgram(files[path], NFFT=1024, Fs=12000, window=lambda data: data * np.hanning(len(data)), 264 | noverlap=512, vmin=10, vmax=200, cmap=COLORMAP) 265 | a_x.set_title(path.rsplit('_', 3)[2]) 266 | a_x.axes.get_yaxis().set_visible(False) 267 | for i in range(len(scores), len(axs.flat)): 268 | fig.delaxes(axs.flat[i]) 269 | fig.savefig('TDoA_' + str(path.rsplit("_", 3)[1]) + '_temp.pdf', bbox_inches='tight') 270 | 271 | 272 | class OctaveProcessing(threading.Thread): 273 | """ Octave processing routine """ 274 | 275 | def __init__(self, input_file, tdoa_rootdir, log_file): 276 | super(OctaveProcessing, self).__init__() 277 | self.m_file_to_process = input_file 278 | self.tdoa_rootdir = tdoa_rootdir 279 | self.log_file = log_file 280 | 281 | def run(self): 282 | global PROC_PID # stdout 283 | APP.gui.console_window.configure(bg=CONS_B, fg=CONS_F) 284 | octave_errors = [b'index-out-of-bounds', b'< 2 good stations found', b'Octave:nonconformant - args', 285 | b'n_stn=2 is not supported', b'resample.m: p and q must be positive integers', 286 | b'Octave:invalid-index', b'incomplete \'data\' chunk', 287 | b'reshape: can\'t reshape 0x0 array to 242x258 array', b'malformed filename:', 288 | b'element number 1 undefined in return list'] 289 | proc = subprocess.Popen(['octave-cli', self.m_file_to_process], cwd=self.tdoa_rootdir, stderr=subprocess.STDOUT, 290 | stdout=subprocess.PIPE, shell=False) 291 | PROC_PID = proc.pid 292 | if PROC_PID: 293 | APP.gui.writelog("ultimateTDoA process started.") 294 | logfile = open(self.log_file, 'w') 295 | if sys.version_info[0] == 2: 296 | for octave_output in proc.stdout: 297 | logfile.write(octave_output) 298 | if any(x in octave_output for x in octave_errors): 299 | APP.gui.console_window.configure(bg='#800000', fg='white') 300 | ProcessFailed(octave_output).start() 301 | proc.terminate() 302 | if b"finished" in octave_output: 303 | logfile.close() 304 | ProcessFinished(self.log_file).start() 305 | proc.terminate() 306 | else: 307 | octave_output = proc.communicate()[0] 308 | logfile.write(str(octave_output, 'utf-8')) 309 | if any(x in octave_output for x in octave_errors): 310 | APP.gui.console_window.configure(bg='#800000', fg='white') 311 | ProcessFailed(octave_output).start() 312 | proc.terminate() 313 | if b"finished" in octave_output: 314 | logfile.close() 315 | ProcessFinished(self.log_file).start() 316 | proc.terminate() 317 | proc.wait() 318 | 319 | 320 | class ProcessFailed(threading.Thread): 321 | """ The actions to perform when a TDoA run has failed. """ 322 | 323 | def __init__(self, returned_error): 324 | super(ProcessFailed, self).__init__() 325 | self.Returned_error = returned_error 326 | 327 | def run(self): 328 | global tdoa_in_progress # TDoA process status 329 | APP.gui.writelog( 330 | "TDoA process error.\n" + bytes.decode(self.Returned_error).rsplit(",\n\t\"stack", 1)[0].replace( 331 | "{\n\t\"identifier\": \"\",\n\t", "")) 332 | APP.gui.compute_button.configure(text="Compute") 333 | tdoa_in_progress = 0 334 | 335 | 336 | class ProcessFinished(threading.Thread): 337 | """ The actions to perform when a TDoA run has finished. """ 338 | 339 | def __init__(self, log_file): 340 | super(ProcessFinished, self).__init__() 341 | self.log_file = log_file 342 | 343 | def run(self): 344 | global tdoa_in_progress # TDoA process status 345 | APP.gui.compute_button.configure(text="Compute") 346 | tdoa_in_progress = 0 347 | with open(os.getcwd() + os.sep + self.log_file, 'r') as read_file: 348 | content = read_file.readlines() 349 | for line in content: 350 | if "last_gnss_fix" in line: 351 | APP.gui.writelog(" " + " ".join(line.rstrip().rsplit(" ", 5)[2:])) 352 | if "position" in line: 353 | APP.gui.writelog(" " + line.rstrip()) 354 | if "tdoa_plot_map_combined" in line: 355 | APP.gui.writelog(" " + line.rstrip()) 356 | APP.gui.writelog("TDoA process finished successfully.") 357 | if open_pdf.get() == 1: 358 | if platform.system() == "Windows": 359 | os.system('start ' + sorted(glob.iglob('*.pdf'), key=os.path.getctime)[-1]) 360 | elif platform.system() == "Darwin": 361 | subprocess.Popen(["open", os.getcwd() + os.sep + sorted(glob.iglob('*.pdf'), key=os.path.getctime)[-1]]) 362 | else: 363 | subprocess.Popen( 364 | ["xdg-open", os.getcwd() + os.sep + sorted(glob.iglob('*.pdf'), key=os.path.getctime)[-1]]) 365 | for spec_file in glob.glob(os.getcwd() + os.sep + "*temp.pdf"): 366 | os.remove(spec_file) 367 | 368 | 369 | class FillMapWithNodes(threading.Thread): 370 | """ process to display the nodes on the World Map. """ 371 | 372 | def __init__(self, parent): 373 | super(FillMapWithNodes, self).__init__() 374 | self.parent = parent 375 | 376 | def run(self): 377 | """ ultimate interface process to display the nodes on the World Map. """ 378 | global tag_list, node_file, node_list, deleted_file, ranking_scale 379 | tag_list = [] 380 | node_list = [] 381 | node_file = [] 382 | deleted_file = 0 383 | ranking_scale = [] 384 | point_list = [] 385 | for wavfiles in glob.glob('*.wav'): 386 | if PlotIQ.has_gps(wavfiles): 387 | node_snr_rank = PlotIQ.score(PlotIQ.load(wavfiles)) 388 | ranking_scale.append(node_snr_rank) 389 | tdoa_id = re.search(r'(.*)_(.*)_iq.wav', wavfiles) 390 | node_list.append(tdoa_id.group(2)) 391 | node_file.append(wavfiles) 392 | with open(up(up(os.getcwd())) + os.sep + "gnss_pos" + os.sep + tdoa_id.group(2) + ".txt", "rt") as gnss: 393 | contents = gnss.read() 394 | info = re.search(r'd\.(.+?)\s.+\W.\[(.*),(.*)\], \'host\', \'(.+?)\', \'port\', (.+?)\);', contents) 395 | nodeinfo = dict( 396 | url=(info.group(4) + ":" + info.group(5)), 397 | lat=info.group(2), 398 | lon=info.group(3), 399 | id=info.group(1), 400 | snr=str(node_snr_rank) 401 | ) 402 | point_list.append([nodeinfo, node_snr_rank]) 403 | else: 404 | os.rename(wavfiles, wavfiles + ".nogps") 405 | deleted_file += 1 406 | if 'APP' in globals(): 407 | APP.gui.writelog(str(len(node_file)) + " nodes are available for this run.") 408 | if deleted_file > 0: 409 | APP.gui.writelog(str(deleted_file) + " node(s) excluded because of missing GPS data.") 410 | for it in point_list: 411 | self.add_point(it[0], (it[1] - min(ranking_scale)) * (1/(max(ranking_scale) - min(ranking_scale)) * 255)) 412 | 413 | @staticmethod 414 | def convert_lat(lat): 415 | """ Convert the real node latitude coordinates to adapt to GUI window map geometry. """ 416 | # nodes are between LATITUDE 0 and 90N 417 | if float(lat) > 0: 418 | return 990 - (float(lat) * 11) 419 | # nodes are between LATITUDE 0 and 60S 420 | return 990 + (float(0 - float(lat)) * 11) 421 | 422 | @staticmethod 423 | def convert_lon(lon): 424 | """ Convert the real node longitude coordinates to adapt to GUI window map geometry. """ 425 | return 1910 + ((float(lon) * 1910) / 180) 426 | 427 | def add_point(self, node_db_data, snr_rank): 428 | """ Process that add node icons over the World map. """ 429 | global tag_list 430 | mykeys = ['url', 'id', 'lat', 'lon', 'snr'] 431 | node_lat = self.convert_lat(node_db_data["lat"]) 432 | node_lon = self.convert_lon(node_db_data["lon"]) 433 | node_tag = str('$'.join([node_db_data[x] for x in mykeys])) 434 | node_tag = node_tag.rsplit('$', 1)[0] + "$%g" % round(snr_rank, 0) 435 | ic_size = int(ICONSIZE) 436 | try: 437 | if ICONTYPE == 0: 438 | self.parent.canvas.create_oval(node_lon - ic_size, node_lat - ic_size, node_lon + ic_size, 439 | node_lat + ic_size, fill=self.color_variant(snr=snr_rank), tag=node_tag) 440 | else: 441 | self.parent.canvas.create_rectangle(node_lon - ic_size, node_lat - ic_size, node_lon + ic_size, 442 | node_lat + ic_size, fill=self.color_variant(snr=snr_rank), 443 | tag=node_tag) 444 | self.parent.canvas.tag_bind(node_tag, "", self.parent.onclickleft) 445 | tag_list.append(node_tag) 446 | except NameError: 447 | print("OOPS - Error in adding the point to the map") 448 | 449 | def delete_point(self, map_definition): 450 | """ Map presets deletion process. """ 451 | self.parent.canvas.delete(map_definition) 452 | 453 | @staticmethod 454 | def color_variant(snr): 455 | green_val = min([255, max([0, int(snr)])]) 456 | return "#00" + "".join("0" + hex(green_val)[2:] if len(hex(green_val)[2:]) < 2 else hex(green_val)[2:]) + "00" 457 | 458 | @staticmethod 459 | def get_font_color(font_color): 460 | """ Adapting the foreground font color regarding background luminosity. 461 | stackoverflow questions/946544/good-text-foreground-color-for-a-given-background-color """ 462 | rgb_hex = [font_color[x:x + 2] for x in [1, 3, 5]] 463 | threshold = 120 # default = 120 464 | if int(rgb_hex[0], 16) * 0.299 + int(rgb_hex[1], 16) * 0.587 + int(rgb_hex[2], 16) * 0.114 > threshold: 465 | return "#000000" 466 | # else: 467 | return "#ffffff" 468 | # if (red*0.299 + green*0.587 + blue*0.114) > 186 use #000000 else use #ffffff 469 | 470 | def node_sel_active(self, node_mac): 471 | """ Adding additionnal highlight on node icon. """ 472 | for node_tag_item in tag_list: 473 | if node_mac in node_tag_item: 474 | tmp_latlon = node_tag_item.rsplit("$", 4) 475 | tmp_lat = self.convert_lat(tmp_latlon[2]) 476 | tmp_lon = self.convert_lon(tmp_latlon[3]) 477 | is_delta = int(ICONSIZE) + 1 478 | if ICONTYPE == 0: 479 | self.parent.canvas.create_oval(tmp_lon - is_delta, tmp_lat - is_delta, tmp_lon + is_delta, 480 | tmp_lat + is_delta, fill='', outline=HIGHLIGHT, 481 | tag=node_tag_item + "$#") 482 | else: 483 | self.parent.canvas.create_rectangle(tmp_lon - is_delta, tmp_lat - is_delta, tmp_lon + is_delta, 484 | tmp_lat + is_delta, fill='', outline=HIGHLIGHT, 485 | tag=node_tag_item + "$#") 486 | self.parent.canvas.tag_bind(node_tag_item + "$#", "", self.parent.onclickleft) 487 | 488 | def node_selection_inactive(self, node_mac): 489 | """ Removing additionnal highlight on selected node icon. """ 490 | for node_tag_item in tag_list: 491 | if node_mac in node_tag_item: 492 | self.parent.canvas.tag_unbind(node_tag_item + "$#", "") 493 | self.parent.canvas.delete(node_tag_item + "$#") 494 | 495 | def node_selection_inactiveall(self): 496 | """ Removing ALL additionnal highlights on selected nodes icons. """ 497 | for node_tag_item in tag_list: 498 | self.parent.canvas.tag_unbind(node_tag_item + "$#", "") 499 | self.parent.canvas.delete(node_tag_item + "$#") 500 | 501 | 502 | class GuiCanvas(Frame): 503 | """ Process that creates the GUI map canvas, enabling move & zoom on a picture. 504 | source: stackoverflow.com/questions/41656176/tkinter-canvas-zoom-move-pan?noredirect=1&lq=1 """ 505 | 506 | def __init__(self, parent): 507 | Frame.__init__(self, parent=None) 508 | # tip: GuiCanvas is member1 509 | parent.call('wm', 'iconphoto', parent, PhotoImage(file='../../../icon.gif')) 510 | global fulllist, mapboundaries_set, map_preset, selectedcity 511 | global lat_min_map, lat_max_map, lon_min_map, lon_max_map, selectedlat, selectedlon 512 | fulllist = [] 513 | mapboundaries_set = None 514 | map_preset = 1 515 | ReadCfg().read_cfg() 516 | parent.geometry(GUI) 517 | parent.geometry('+0+60') 518 | self.x = self.y = 0 519 | # Create canvas and put image on it 520 | self.canvas = Canvas(self.master, highlightthickness=0) 521 | self.canvas.grid(row=0, column=0, sticky='nswe') 522 | self.canvas.update() # wait till canvas is created 523 | # Make the canvas expandable 524 | self.master.rowconfigure(0, weight=1) 525 | self.master.columnconfigure(0, weight=1) 526 | # Bind events to the Canvas 527 | self.canvas.bind('', self.show_image) # canvas is resized 528 | self.canvas.bind('', self.move_from) # map move 529 | self.canvas.bind('', self.move_to) # map move 530 | # self.canvas.bind_all('', self.wheel) # Windows Zoom 531 | # self.canvas.bind('', self.wheel) # Linux Zoom 532 | # self.canvas.bind('', self.wheel) # Linux Zoom 533 | self.canvas.bind("", self.on_button_press) # red rectangle selection 534 | self.canvas.bind("", self.on_move_press) # red rectangle selection 535 | self.canvas.bind("", self.on_button_release) # red rectangle selection 536 | self.image = Image.open(up(up(up(os.getcwd()))) + os.sep + DMAP) 537 | self.width, self.height = self.image.size 538 | self.imscale = 1.0 # scale for the image 539 | self.delta = 2.0 # zoom magnitude 540 | # Put image into container rectangle and use it to set proper coordinates to the image 541 | self.container = self.canvas.create_rectangle(0, 0, self.width, self.height, width=0) 542 | self.canvas.config(scrollregion=(0, 0, self.width, self.height)) 543 | self.rect = None 544 | self.start_x = None 545 | self.start_y = None 546 | for mfil_e in glob.glob(os.getcwd() + os.sep + "proc*.empty"): 547 | m = open(mfil_e, 'r') 548 | filedata = m.read() 549 | m.close() 550 | try: 551 | mapposx = re.search(r"##\s(.*),(.*)", filedata) 552 | tdoa_x0 = mapposx.group(1) 553 | tdoa_y0 = mapposx.group(2) 554 | except AttributeError: 555 | tdoa_x0 = DX0 556 | tdoa_y0 = DY0 557 | lat = re.search(r"lat_range', \[([-]?[0-9]{1,2}(\.[0-9]*)?) ?([-]?[0-9]{1,2}(\.[0-9]*|))?\]", filedata) 558 | lon = re.search(r"lon_range', \[([-]?[0-9]{1,3}(\.[0-9]*)?) ?([-]?[0-9]{1,3}(\.[0-9]*|))?\]", filedata) 559 | lat_min_map = lat.group(1) 560 | lat_max_map = lat.group(3) 561 | lon_min_map = lon.group(1) 562 | lon_max_map = lon.group(3) 563 | place_regexp_coords = re.search(r"('known_location', struct\('coord', \[([-]?[0-9]{1,2}(\.[0-9]*)?|0) ?([-]?[0-9]{1,3}(\.[0-9]*)?|0)\],)", filedata) 564 | selectedlat = place_regexp_coords.group(2) 565 | selectedlon = place_regexp_coords.group(4) 566 | selectedcity = re.search(r"('name',\s '(.+)'\),)", filedata).group(2) 567 | self.canvas.create_rectangle(self.convert_lon(lon_min_map), self.convert_lat(lat_max_map), 568 | self.convert_lon(lon_max_map), self.convert_lat(lat_min_map), outline='red', 569 | tag="mappreset") 570 | self.create_known_point(selectedlat, selectedlon, selectedcity) 571 | self.canvas.scan_dragto(-int(tdoa_x0.split('.')[0]), -int(tdoa_y0.split('.')[0]), gain=1) 572 | FillMapWithNodes(self).start() 573 | 574 | @staticmethod 575 | def convert_lat(lat): 576 | """ Convert the real node latitude coordinates to adapt to GUI window map geometry. """ 577 | # nodes are between LATITUDE 0 and 90N 578 | if float(lat) > 0: 579 | return 990 - (float(lat) * 11) 580 | # nodes are between LATITUDE 0 and 60S 581 | return 990 + (float(0 - float(lat)) * 11) 582 | 583 | @staticmethod 584 | def convert_lon(lon): 585 | """ Convert the real node longitude coordinates to adapt to GUI window map geometry. """ 586 | return 1910 + ((float(lon) * 1910) / 180) 587 | 588 | def on_button_press(self, event): 589 | """ Red rectangle selection drawing on the World map. """ 590 | global map_preset 591 | self.delete_point("mappreset") 592 | if map_preset == 1: 593 | self.rect = None 594 | map_preset = 0 595 | self.start_x = self.canvas.canvasx(event.x) 596 | self.start_y = self.canvas.canvasy(event.y) 597 | # create rectangle if not yet exist 598 | if not self.rect: 599 | self.rect = self.canvas.create_rectangle(self.x, self.y, 1, 1, outline='red', tag="mapmanual") 600 | 601 | def on_move_press(self, event): 602 | """ Get the Map boundaries red rectangle selection coordinates. """ 603 | global lat_min_map, lat_max_map, lon_min_map, lon_max_map 604 | if image_scale == 1: 605 | if map_preset == 1: 606 | pass 607 | else: 608 | cur_x = self.canvas.canvasx(event.x) 609 | cur_y = self.canvas.canvasy(event.y) 610 | lonmin = round(((self.start_x - 1910) * 180) / 1910, 1) 611 | lonmax = round(((cur_x - 1910) * 180) / 1910, 1) 612 | latmax = round(0 - ((cur_y - 990) / 11), 1) 613 | latmin = round((self.start_y - 990) / 11, 1) 614 | 615 | if cur_x > self.start_x and cur_y > self.start_y: 616 | lat_max_map = 0 - latmin 617 | lat_min_map = latmax 618 | lon_max_map = lonmax 619 | lon_min_map = lonmin 620 | 621 | if cur_x < self.start_x and cur_y > self.start_y: 622 | lat_max_map = 0 - latmin 623 | lat_min_map = latmax 624 | lon_max_map = lonmin 625 | lon_min_map = lonmax 626 | 627 | if cur_x > self.start_x and cur_y < self.start_y: 628 | lat_max_map = latmax 629 | lat_min_map = 0 - latmin 630 | lon_max_map = lonmax 631 | lon_min_map = lonmin 632 | 633 | if cur_x < self.start_x and cur_y < self.start_y: 634 | lat_max_map = latmax 635 | lat_min_map = 0 - latmin 636 | lon_max_map = lonmin 637 | lon_min_map = lonmax 638 | 639 | w_canva, h_canva = self.canvas.winfo_width(), self.canvas.winfo_height() 640 | if event.x > 0.98 * w_canva: 641 | self.canvas.xview_scroll(1, 'units') 642 | elif event.x < 0.02 * w_canva: 643 | self.canvas.xview_scroll(-1, 'units') 644 | if event.y > 0.98 * h_canva: 645 | self.canvas.yview_scroll(1, 'units') 646 | elif event.y < 0.02 * h_canva: 647 | self.canvas.yview_scroll(-1, 'units') 648 | # expand rectangle as you drag the mouse 649 | self.canvas.coords(self.rect, self.start_x, self.start_y, cur_x, cur_y) 650 | self.show_image() 651 | else: 652 | pass 653 | 654 | @staticmethod 655 | def on_button_release(event): 656 | """ When Mouse right button is released (map boundaries set or ultimateTDoA set). """ 657 | 658 | global mapboundaries_set, map_preset # lon_min_map, lon_max_map, lat_min_map, lat_max_map 659 | global map_manual 660 | if map_preset == 1 and map_manual == 0: 661 | pass 662 | else: 663 | try: 664 | mapboundaries_set = 1 665 | map_manual = 1 666 | except NameError: 667 | pass 668 | 669 | def create_known_point(self, y_kwn, x_kwn, n): 670 | """ Map known place creation process, works only when self.imscale = 1.0 """ 671 | # city coordinates y & x (degrees) converted to pixels 672 | ic_size = int(ICONSIZE) 673 | xx0 = (1910 + ((float(x_kwn) * 1910) / 180)) - ic_size 674 | xx1 = (1910 + ((float(x_kwn) * 1910) / 180)) + ic_size 675 | if float(y_kwn) > 0: # point is located in North Hemisphere 676 | yy0 = (990 - (float(y_kwn) * 11)) - ic_size 677 | yy1 = (990 - (float(y_kwn) * 11)) + ic_size 678 | else: # point is located in South Hemisphere 679 | yy0 = (990 + (float(0 - float(y_kwn)) * 11)) - ic_size 680 | yy1 = (990 + (float(0 - float(y_kwn)) * 11)) + ic_size 681 | self.canvas.create_rectangle(xx0, yy0, xx1, yy1, fill=POICOLOR, outline="black", activefill=POICOLOR, 682 | tag="#POI") 683 | self.canvas.create_text(xx0, yy0 - 10, text=selectedcity.rsplit(' (')[0].replace("_", " "), justify='center', 684 | fill=POICOLOR, tag="#POI") 685 | 686 | def unselect_allpoint(self): 687 | """ Calling process that remove additionnal highlight on all selected nodes. """ 688 | FillMapWithNodes(self).node_selection_inactiveall() 689 | 690 | def delete_point(self, n): 691 | """ KnownPoint deletion process. """ 692 | FillMapWithNodes(self).delete_point(n.rsplit(' (')[0]) 693 | 694 | def onclickleft(self, event): 695 | """ Left Mouse Click bind on the World map. """ 696 | global HOST, node_file, node_list 697 | menu0 = Menu(self, tearoff=0, fg="black", bg=BGC, font='TkFixedFont 7') # node overlap list menu 698 | menu1 = Menu(self, tearoff=0, fg="black", bg=BGC, font='TkFixedFont 7') 699 | # search for overlapping nodes 700 | overlap_range = ICONSIZE * 4 701 | overlap_rect = (self.canvas.canvasx(event.x) - overlap_range), (self.canvas.canvasy(event.y) - overlap_range), ( 702 | self.canvas.canvasx(event.x) + overlap_range), (self.canvas.canvasy(event.y) + overlap_range) 703 | node_overlap_match = self.canvas.find_enclosed(*overlap_rect) 704 | overlap_list = [] 705 | for item_o in list(node_overlap_match): 706 | if "$#" not in self.canvas.gettags(self.canvas.find_withtag(item_o))[0]: 707 | overlap_list.append(item_o) 708 | if len(node_overlap_match) > 1 and len(overlap_list) != 1: # node icon overlap found, displays menu0 709 | for el1, el2 in enumerate(node_overlap_match): 710 | if "$#" not in str(self.canvas.gettags(el2)): # dont display node highlight tags 711 | HOST = self.canvas.gettags(self.canvas.find_withtag(el2))[0] 712 | # mykeys = ['url', 'id', 'lat', 'lon', 'snr'] 713 | # n_field 0 1 2 3 4 714 | n_field = HOST.rsplit("$", 4) 715 | cbg = FillMapWithNodes.color_variant(snr=n_field[4]) 716 | dfg = FillMapWithNodes.get_font_color(cbg) 717 | # check if node is already in the TDoA node listing 718 | if len([el for el in fulllist if n_field[1] == el.rsplit("$", 3)[2]]) != 1: 719 | name = n_field[1] 720 | else: 721 | name = "✔ " + n_field[1] 722 | menu0.add_command(label=name, background=cbg, foreground=dfg, 723 | command=lambda x=HOST: self.create_node_menu(x, event.x_root, event.y_root, 724 | menu1)) 725 | else: 726 | pass 727 | menu0.tk_popup(event.x_root, event.y_root) 728 | else: 729 | HOST = self.canvas.gettags(self.canvas.find_withtag(CURRENT))[0] 730 | self.create_node_menu(HOST, event.x_root, event.y_root, menu1) 731 | 732 | def create_node_menu(self, kiwinodetag, popx, popy, menu): 733 | n_field = kiwinodetag.rsplit("$", 5) 734 | matches = [el for el in fulllist if n_field[1] == el.rsplit("$", 3)[2]] 735 | cbg = FillMapWithNodes.color_variant(snr=n_field[4]) 736 | dfg = FillMapWithNodes.get_font_color(cbg) 737 | # show IQ spectrogram in GUI (PlotIQ mode 0) 738 | PlotIQ(node_file[node_list.index(n_field[1].replace("/", ""))], 0, 0).run() 739 | if len(matches) != 1: 740 | menu.add_command(label="Add " + n_field[1] + " for TDoA process", background=cbg, foreground=dfg, 741 | font="TkFixedFont 7 bold", command=lambda *args: self.populate("add", n_field)) 742 | elif len(matches) == 1: 743 | menu.add_command(label="Remove " + n_field[1] + " from TDoA process]", background=cbg, foreground=dfg, 744 | font="TkFixedFont 7 bold", command=lambda: self.populate("del", n_field)) 745 | menu.tk_popup(int(popx), int(popy)) # popup placement // node icon 746 | 747 | def populate(self, action, sel_node_tag): 748 | """ TDoA listing node populate/depopulate process. """ 749 | if action == "add": 750 | if len(fulllist) < 6: 751 | fulllist.append( 752 | sel_node_tag[0].rsplit(':')[0] + "$" + sel_node_tag[0].rsplit(':')[1] + "$" + sel_node_tag[ 753 | 1].replace("/", "")) 754 | FillMapWithNodes(self).node_sel_active(sel_node_tag[0]) 755 | else: 756 | tkMessageBox.showinfo(title=" ¯\\_(ツ)_/¯", message="6 nodes Maximum !") 757 | elif action == "del": 758 | fulllist.remove( 759 | sel_node_tag[0].rsplit(':')[0] + "$" + sel_node_tag[0].rsplit(':')[1] + "$" + sel_node_tag[1].replace( 760 | "/", "")) 761 | FillMapWithNodes(self).node_selection_inactive(sel_node_tag[0]) 762 | if fulllist: 763 | APP.title(VERSION + "| " + FREQUENCY + ALEID + " - Selected nodes [" + str( 764 | len(fulllist)) + "] : " + '/'.join(str(p).rsplit('$')[2] for p in fulllist)) 765 | else: 766 | APP.title(VERSION + "| " + FREQUENCY + ALEID) 767 | 768 | def move_from(self, event): 769 | """ Move from. """ 770 | self.canvas.scan_mark(event.x, event.y) 771 | 772 | def move_to(self, event): 773 | """ Move to. """ 774 | if 'HOST' in globals() and "current" not in self.canvas.gettags(self.canvas.find_withtag(CURRENT))[0]: 775 | pass 776 | elif "current" in self.canvas.gettags(self.canvas.find_withtag(CURRENT))[0]: 777 | self.canvas.scan_dragto(event.x, event.y, gain=1) 778 | self.show_image() # redraw the image 779 | 780 | def wheel(self, event): 781 | """ Routine for mouse wheel actions. """ 782 | x_eve = self.canvas.canvasx(event.x) 783 | y_eve = self.canvas.canvasy(event.y) 784 | global image_scale 785 | bbox = self.canvas.bbox(self.container) # get image area 786 | if bbox[0] < x_eve < bbox[2] and bbox[1] < y_eve < bbox[3]: 787 | pass # Ok! Inside the image 788 | else: 789 | return # zoom only inside image area 790 | scale = 1.0 791 | # Respond to Linux (event.num) or Windows (event.delta) wheel event 792 | if event.num == 5 or event.delta == -120: # scroll down 793 | i = min(self.width, self.height) 794 | if int(i * self.imscale) < 2000: 795 | return # block zoom if image is less than 2000 pixels 796 | self.imscale /= self.delta 797 | scale /= self.delta 798 | if event.num == 4 or event.delta == 120: # scroll up 799 | i = min(self.canvas.winfo_width(), self.canvas.winfo_height()) 800 | if i < self.imscale: 801 | return # 1 pixel is bigger than the visible area 802 | self.imscale *= self.delta 803 | scale *= self.delta 804 | # rescale all canvas objects 805 | # scale = 2.0 or 0.5 806 | image_scale = self.imscale 807 | # APP.gui.label04.configure(text="Map Zoom : " + str(int(image_scale))) 808 | self.canvas.scale('all', x_eve, y_eve, scale, scale) 809 | # self.canvas.scale('') 810 | self.show_image() 811 | 812 | def show_image(self, event=None): 813 | """ Creating the canvas with the picture. """ 814 | global b_box2 815 | b_box1 = self.canvas.bbox(self.container) # get image area 816 | # Remove 1 pixel shift at the sides of the bbox1 817 | b_box1 = (b_box1[0] + 1, b_box1[1] + 1, b_box1[2] - 1, b_box1[3] - 1) 818 | b_box2 = (self.canvas.canvasx(0), # get visible area of the canvas 819 | self.canvas.canvasy(0), 820 | self.canvas.canvasx(self.canvas.winfo_width()), 821 | self.canvas.canvasy(self.canvas.winfo_height())) 822 | bbox = [min(b_box1[0], b_box2[0]), min(b_box1[1], b_box2[1]), # get scroll region box 823 | max(b_box1[2], b_box2[2]), max(b_box1[3], b_box2[3])] 824 | if bbox[0] == b_box2[0] and bbox[2] == b_box2[2]: # whole image in the visible area 825 | bbox[0] = b_box1[0] 826 | bbox[2] = b_box1[2] 827 | if bbox[1] == b_box2[1] and bbox[3] == b_box2[3]: # whole image in the visible area 828 | bbox[1] = b_box1[1] 829 | bbox[3] = b_box1[3] 830 | self.canvas.configure(scrollregion=bbox) # set scroll region 831 | x_1 = max(b_box2[0] - b_box1[0], 0) # get coordinates (x1,y1,x2,y2) of the image tile 832 | y_1 = max(b_box2[1] - b_box1[1], 0) 833 | x_2 = min(b_box2[2], b_box1[2]) - b_box1[0] 834 | y_2 = min(b_box2[3], b_box1[3]) - b_box1[1] 835 | if int(x_2 - x_1) > 0 and int(y_2 - y_1) > 0: # show image if it in the visible area 836 | x = min(int(x_2 / self.imscale), self.width) # sometimes it is larger on 1 pixel... 837 | y = min(int(y_2 / self.imscale), self.height) # ...and sometimes not 838 | image = self.image.crop((int(x_1 / self.imscale), int(y_1 / self.imscale), x, y)) 839 | imagetk = ImageTk.PhotoImage(image.resize((int(x_2 - x_1), int(y_2 - y_1)))) 840 | imageid = self.canvas.create_image(max(b_box2[0], b_box1[0]), max(b_box2[1], b_box1[1]), 841 | anchor='nw', image=imagetk) 842 | self.canvas.lower(imageid) # set image into background 843 | self.canvas.imagetk = imagetk # keep an extra reference to prevent garbage-collection 844 | 845 | 846 | class MainWindow(Frame): 847 | """ GUI design definitions. """ 848 | 849 | def __init__(self, parent): 850 | Frame.__init__(self, parent) 851 | self.member1 = GuiCanvas(parent) 852 | ReadKnownPointFile().start() 853 | global image_scale, node_file 854 | global map_preset, tdoa_in_progress, open_pdf 855 | global lat_min_map, lat_max_map, lon_min_map, lon_max_map 856 | dfgc = '#a3a3a3' # GUI (disabled) foreground color 857 | image_scale = 1 858 | la_f = Font(family="TkFixedFont", size=7, weight="bold") 859 | map_preset = 0 860 | tdoa_in_progress = 0 861 | open_pdf = IntVar(self, value=1) 862 | # Control panel background 863 | self.label0 = Label(parent) 864 | self.label0.place(relx=0, rely=0.64, relheight=0.4, relwidth=1) 865 | self.label0.configure(bg=BGC, fg=FGC, width=214) 866 | # get values from the original .empty proc file 867 | for mfile in glob.glob(os.getcwd() + os.sep + "proc*.empty"): 868 | f = open(mfile, 'r') 869 | file_d = f.read() 870 | f.close() 871 | use_constraints_origin = re.search(r"(\s{16}'use_constraints', (.+),)", file_d).group(2) 872 | algo_origin = re.search(r"(\s{16}'new', (.+))", file_d).group(2) 873 | mapbox_style = re.search(r"(?:\S+ ){15}(\S+)", file_d).group(1) 874 | self.label01 = Label(parent) 875 | self.label01.place(x=14, y=14, height=14, width=105) 876 | self.label01.configure(bg="black", font=la_f, anchor="w", fg="deepskyblue2", 877 | text="Algorithm: 2020" if algo_origin == "true" else "Algorithm: 2018") 878 | self.label02 = Label(parent) 879 | self.label02.place(x=14, y=28, height=14, width=105) 880 | self.label02.configure(bg="black", font=la_f, anchor="w", fg="deepskyblue2", 881 | text="Constraints: yes" if use_constraints_origin == "true" else "Constraints: no") 882 | self.label03 = Label(parent) 883 | self.label03.place(x=14, y=42, height=14, width=105) 884 | self.label03.configure(bg="black", font=la_f, anchor="w", fg="deepskyblue2", text="Map: " + mapbox_style) 885 | 886 | # Compute button 887 | self.compute_button = Button(parent) 888 | self.compute_button.place(relx=0.61, rely=0.65, height=64, relwidth=0.115) 889 | self.compute_button.configure(activebackground="#d9d9d9", activeforeground="#000000", bg='#d9d9d9', 890 | disabledforeground=dfgc, fg="#000000", highlightbackground="#d9d9d9", 891 | highlightcolor="#000000", pady="0", text="Compute", 892 | command=self.start_stop_tdoa) 893 | 894 | # Trim_iq button 895 | self.trim_iq_button = Button(parent) 896 | self.trim_iq_button.place(relx=0.61, rely=0.75, height=24, relwidth=0.115) 897 | self.trim_iq_button.configure(activebackground="lightblue3", activeforeground="#000000", bg="lightblue", 898 | disabledforeground=dfgc, fg="#000000", highlightbackground="#d9d9d9", 899 | highlightcolor="#000000", pady="0", text="run trim_iq.py", 900 | command=lambda: APP.gui.trimbutton(), state="normal") 901 | 902 | # Purge node listing button 903 | self.purge_button = Button(parent) 904 | self.purge_button.place(relx=0.61, rely=0.8, height=24, relwidth=0.115) 905 | self.purge_button.configure(activebackground="red", activeforeground="#000000", bg="red3", 906 | disabledforeground=dfgc, fg="#000000", highlightbackground="#d9d9d9", 907 | highlightcolor="#000000", pady="0", text="purge node list", command=self.purgenode, 908 | state="normal") 909 | 910 | # Auto open TDoA PDF result file 911 | self.open_pdf_checkbox = Checkbutton(parent) 912 | self.open_pdf_checkbox.place(relx=0.61, rely=0.85, height=21, relwidth=0.11) 913 | self.open_pdf_checkbox.configure(bg=BGC, fg=FGC, activebackground=BGC, activeforeground=FGC, 914 | font="TkFixedFont 8", width=214, selectcolor=BGC, 915 | text="open pdf automatically?", 916 | anchor="w", variable=open_pdf, command=None) 917 | 918 | # Known places search textbox 919 | self.choice = Entry(parent) 920 | self.choice.place(relx=0.01, rely=0.95, height=21, relwidth=0.18) 921 | self.choice.insert(0, "TDoA map city/site search here") 922 | self.listbox = Listbox(parent) 923 | self.listbox.place(relx=0.2, rely=0.95, height=21, relwidth=0.3) 924 | 925 | # Console window 926 | self.console_window = Text(parent) 927 | self.console_window.place(relx=0.005, rely=0.65, relheight=0.285, relwidth=0.6) 928 | self.console_window.configure(bg=CONS_B, font="TkTextFont", fg=CONS_F, highlightbackground=BGC, 929 | highlightcolor=FGC, insertbackground=FGC, selectbackground="#c4c4c4", 930 | selectforeground=FGC, undo="1", width=970, wrap="word") 931 | 932 | # plot IQ preview window 933 | self.plot_iq_button = Button(parent, command=lambda: APP.gui.openinbrowser( 934 | [tag_list[tag_list.index(x)].rsplit("$", 4)[0] for x in tag_list if CLICKEDNODE in x], 935 | ''.join(re.match(r"(\d+.\d+)", FREQUENCY).group(1)))) 936 | self.plot_iq_button.place(relx=0.73, rely=0.65, height=240, width=320) 937 | 938 | # Adding some texts to console window at program start 939 | self.writelog("This is " + VERSION + ", a GUI written for python 2/3 with Tk") 940 | 941 | # GUI topbar menus 942 | menubar = Menu(self) 943 | parent.config(menu=menubar) 944 | menu_1 = Menu(menubar, tearoff=0) 945 | menubar.add_cascade(label="Mapbox style", menu=menu_1) 946 | menu_1.add_command(label="streets", command=lambda *args: self.mapbox_style("streets-v11")) 947 | menu_1.add_command(label="outdoors", command=lambda *args: self.mapbox_style("outdoors-v11")) 948 | menu_1.add_command(label="light", command=lambda *args: self.mapbox_style("light-v10")) 949 | menu_1.add_command(label="dark", command=lambda *args: self.mapbox_style("dark-v10")) 950 | menu_1.add_command(label="satellite", command=lambda *args: self.mapbox_style("satellite-v9")) 951 | menu_1.add_command(label="satellite-streets", command=lambda *args: self.mapbox_style("satellite-streets-v11")) 952 | menu_2 = Menu(menubar, tearoff=0) 953 | menubar.add_cascade(label="Map Presets", menu=menu_2) 954 | menu_2.add_command(label="Europe", command=lambda *args: self.map_preset("EU")) 955 | menu_2.add_command(label="Africa", command=lambda *args: self.map_preset("AF")) 956 | menu_2.add_command(label="Middle-East", command=lambda *args: self.map_preset("ME")) 957 | menu_2.add_command(label="South Asia", command=lambda *args: self.map_preset("SAS")) 958 | menu_2.add_command(label="South-East Asia", command=lambda *args: self.map_preset("SEAS")) 959 | menu_2.add_command(label="East Asia", command=lambda *args: self.map_preset("EAS")) 960 | menu_2.add_command(label="North America", command=lambda *args: self.map_preset("NAM")) 961 | menu_2.add_command(label="Central America", command=lambda *args: self.map_preset("CAM")) 962 | menu_2.add_command(label="South America", command=lambda *args: self.map_preset("SAM")) 963 | menu_2.add_command(label="Oceania", command=lambda *args: self.map_preset("O")) 964 | menu_2.add_command(label="West Russia", command=lambda *args: self.map_preset("WR")) 965 | menu_2.add_command(label="East Russia", command=lambda *args: self.map_preset("ER")) 966 | menu_2.add_command(label="USA", command=lambda *args: self.map_preset("US")) 967 | menu_2.add_command(label="World (use with caution)", command=lambda *args: self.map_preset("W")) 968 | 969 | # TDoA settings menu 970 | menu_3 = Menu(menubar, tearoff=0) 971 | menubar.add_cascade(label="TDoA settings", menu=menu_3) 972 | sm8 = Menu(menu_3, tearoff=0) 973 | sm9 = Menu(menu_3, tearoff=0) 974 | sm10 = Menu(menu_3, tearoff=0) 975 | menu_3.add_cascade(label='plot_kiwi_json', menu=sm8, underline=0) 976 | menu_3.add_cascade(label='algorithm', menu=sm10, underline=0) 977 | sm8.add_command(label="yes", command=lambda *args: self.tdoa_settings(0)) 978 | sm8.add_command(label="no", command=lambda *args: self.tdoa_settings(1)) 979 | sm9.add_command(label="option: use_constraints = 1", command=lambda *args: self.tdoa_settings(2)) 980 | sm9.add_command(label="option: use_constraints = 0", command=lambda *args: self.tdoa_settings(3)) 981 | sm10.add_cascade(label='former (2018)', menu=sm9, underline=0) 982 | sm10.add_command(label="new (2020)", command=lambda *args: self.tdoa_settings(4)) 983 | 984 | # Various GUI binds 985 | self.listbox_update(my_info1) 986 | self.listbox.bind('<>', self.on_select) 987 | self.choice.bind('', self.resetcity) 988 | self.choice.bind('', self.on_keyrelease) 989 | 990 | @staticmethod 991 | def trimbutton(): 992 | if tkMessageBox.askokcancel("Modify IQ files ?", "Do you want to run the trim_iq script ?", icon="warning"): 993 | TrimIQ(os.getcwd()).start() 994 | 995 | @staticmethod 996 | def openinbrowser(host_port, freq_and_mode): 997 | """ Web browser call to connect on the node (default = IQ mode & fixed zoom level at 8). """ 998 | if len(host_port) == 1: 999 | webbrowser.open_new("http://" + str("".join(host_port)) + "/?f=" + freq_and_mode) 1000 | 1001 | def mapbox_style(self, value): 1002 | global MAP_BOX 1003 | self.label03.configure(text="Map: " + value) 1004 | MAP_BOX = value 1005 | 1006 | def tdoa_settings(self, value): 1007 | global plot_kiwi_json_new, use_constraints_new, algo_new 1008 | if value == 0: 1009 | self.writelog("OPTION: plot_kiwi_json set to YES.") 1010 | plot_kiwi_json_new = "true" 1011 | if value == 1: 1012 | self.writelog("OPTION: plot_kiwi_json set to NO.") 1013 | plot_kiwi_json_new = "false" 1014 | if value == 2: 1015 | self.writelog("OPTION: former TDoA algorithm selected w/ option \'use_constraints\' set to YES.") 1016 | algo_new = "false" 1017 | use_constraints_new = "true" 1018 | self.label01.configure(text="Algorithm: 2018") 1019 | self.label02.configure(text="Constraints: yes") 1020 | if value == 3: 1021 | self.writelog("OPTION: former TDoA algorithm selected w/ option \'use_constraints\' set to NO.") 1022 | algo_new = "false" 1023 | use_constraints_new = "false" 1024 | self.label01.configure(text="Algorithm: 2018") 1025 | self.label02.configure(text="Constraints: no") 1026 | if value == 4: 1027 | self.writelog("OPTION: new TDoA algorithm selected.") 1028 | algo_new = "true" 1029 | use_constraints_new = "false" 1030 | self.label01.configure(text="Algorithm: 2020") 1031 | self.label02.configure(text="Constraints: no") 1032 | 1033 | def map_preset(self, pmap): 1034 | """ Map boundaries static presets. """ 1035 | global mapboundaries_set, lon_min_map, lon_max_map, lat_min_map, lat_max_map 1036 | global sx0, sy0 1037 | global map_preset, map_manual 1038 | if image_scale == 1: 1039 | p_map = [] 1040 | self.member1.delete_point("mappreset") 1041 | for i in range(0, 4): 1042 | p_map.append(CFG["presets(x0/y1/x1/y0)"][pmap][i]) 1043 | sx0 = (1911 + ((float(p_map[0]) * 1911) / 180)) 1044 | sx1 = (1911 + ((float(p_map[2]) * 1911) / 180)) 1045 | if float(p_map[1]) > 0: # point is located in North Hemisphere 1046 | sy0 = (990 - (float(p_map[1]) * 11)) 1047 | sy1 = (990 - (float(p_map[3]) * 11)) 1048 | else: # point is located in South Hemisphere 1049 | sy0 = (990 + (float(0 - (float(p_map[1]) * 11)))) 1050 | sy1 = (990 + (float(0 - float(p_map[3])) * 11)) 1051 | self.member1.canvas.create_rectangle(sx0, sy0, sx1, sy1, tag="mappreset", outline='yellow') 1052 | self.member1.delete_point("mapmanual") 1053 | lon_min_map = p_map[0] 1054 | lat_max_map = p_map[1] 1055 | lon_max_map = p_map[2] 1056 | lat_min_map = p_map[3] 1057 | mapboundaries_set = 1 1058 | map_preset = 1 1059 | map_manual = 0 1060 | else: 1061 | self.writelog("ERROR : The boundaries selection is forbidden unless map un-zoomed.") 1062 | 1063 | def on_keyrelease(self, event): 1064 | """ Known place location listing search management. """ 1065 | value = event.widget.get() 1066 | value = value.strip().lower() 1067 | if value == '': 1068 | data = my_info1 1069 | else: 1070 | data = [] 1071 | for item in my_info1: 1072 | if value in item.lower(): 1073 | data.append(item) 1074 | self.listbox_update(data) 1075 | 1076 | def listbox_update(self, data): 1077 | """ Known place location listing search management. """ 1078 | self.listbox.delete(0, 'end') 1079 | data = sorted(data, key=str.lower) 1080 | for item in data: 1081 | self.listbox.insert('end', item) 1082 | 1083 | def on_select(self, event): 1084 | """ Known place location selection process. """ 1085 | global selectedlat, selectedlon, selectedcity 1086 | try: 1087 | if event.widget.get(event.widget.curselection()) == " ": 1088 | tkMessageBox.showinfo(title=" ¯\\_(ツ)_/¯", message="Type something in the POI Search box first !") 1089 | else: 1090 | selectedcity = event.widget.get(event.widget.curselection()).rsplit(" |", 1)[0] 1091 | selectedlat = str(my_info2[my_info1.index(selectedcity)]) 1092 | selectedlon = str(my_info3[my_info1.index(selectedcity)]) 1093 | self.member1.delete_point(n="#POI") 1094 | self.member1.create_known_point(selectedlat, selectedlon, selectedcity) 1095 | except: 1096 | pass 1097 | 1098 | def resetcity(self, event): 1099 | """ Erase previous known location choice from both textbox input and World map icon and name. """ 1100 | global selectedcity, selectedlat, selectedlon 1101 | self.choice.delete(0, 'end') 1102 | if selectedcity: 1103 | self.member1.delete_point(n="#POI") 1104 | selectedcity = " " 1105 | selectedlat = "-90" 1106 | selectedlon = "180" 1107 | 1108 | def writelog(self, msg): 1109 | """ The main console log text feed. """ 1110 | self.console_window.insert('end -1 lines', 1111 | "[" + str(time.strftime('%H:%M.%S', time.gmtime())) + "] - " + msg + "\n") 1112 | time.sleep(0.01) 1113 | self.console_window.see('end') 1114 | 1115 | def purgenode(self): 1116 | """ Purge ultimateTDoA list process. """ 1117 | if tkMessageBox.askokcancel("Purge node listing ?", "Do you want to purge the listing ?", icon="warning"): 1118 | global fulllist 1119 | fulllist = [] 1120 | APP.title(VERSION + "| " + FREQUENCY + ALEID) 1121 | self.member1.unselect_allpoint() 1122 | 1123 | def start_stop_tdoa(self): 1124 | """ Actions to perform when Compute button is clicked. """ 1125 | global tdoa_in_progress, PROC_PID 1126 | global plot_kiwi_json_new, use_constraints_new, algo_new 1127 | global lon_min_map, lon_max_map, lat_min_map, lat_max_map 1128 | global plot_kiwi_json_origin, use_constraints_origin, algo_origin 1129 | global selectedlat, selectedlon, selectedcity 1130 | if tdoa_in_progress == 1: # Abort TDoA process 1131 | self.purge_button.configure(state="normal") 1132 | try: # kills the octave process 1133 | os.kill(PROC_PID, signal.SIGTERM) 1134 | except (NameError, OSError): 1135 | pass 1136 | try: # and ghostscript 1137 | if platform.system() == "Windows": 1138 | if "gs.exe" in os.popen("tasklist").read(): 1139 | os.system("taskkill /F /IM gs.exe") 1140 | else: 1141 | os.system("killall -9 gs") 1142 | except (NameError, OSError): 1143 | pass 1144 | self.writelog("Octave process has been aborted...") 1145 | tdoa_in_progress = 0 1146 | self.compute_button.configure(text="Compute") 1147 | else: 1148 | if len(fulllist) < 3: # debug 1149 | self.writelog("ERROR : Select at least 3 nodes for TDoA processing !") 1150 | else: 1151 | new_nodes = [] 1152 | file_list = [] 1153 | # Open the proc.empty file and read the lines 1154 | for mfile in glob.glob(os.getcwd() + os.sep + "proc*.empty"): 1155 | f = open(mfile, 'r') 1156 | file_d = f.read() 1157 | f.close() 1158 | # get values from the original .empty proc file 1159 | plot_kiwi_json_origin = re.search(r"(\s{16}'plot_kiwi_json', (.+),)", file_d).group(2) 1160 | use_constraints_origin = re.search(r"(\s{16}'use_constraints', (.+),)", file_d).group(2) 1161 | algo_origin = re.search(r"(\s{16}'new', (.+))", file_d).group(2) 1162 | dir_origin = re.search(r".+/(.+)/", file_d).group(1) 1163 | # Fill the .m file with selected node listing 1164 | i = 1 1165 | for node in fulllist: 1166 | new_nodes.append(" input(" + str(i) + ").fn = fullfile('iq', '" + os.path.basename( 1167 | os.path.dirname(mfile)) + "', '" + node_file[ 1168 | node_list.index(node.rsplit("$", 4)[2])] + "\');") 1169 | file_list.append(node_file[node_list.index(node.rsplit("$", 4)[2])]) 1170 | i += 1 1171 | file_d = file_d.replace(" # nodes", "\n".join(new_nodes)) 1172 | # Create new config block 1173 | pkjn = plot_kiwi_json_new if 'plot_kiwi_json_new' in globals() and plot_kiwi_json_origin != plot_kiwi_json_new else plot_kiwi_json_origin 1174 | uc = use_constraints_new if 'use_constraints_new' in globals() and use_constraints_origin != use_constraints_new else use_constraints_origin 1175 | al = algo_new if 'algo_new' in globals() and algo_origin != algo_new else algo_origin 1176 | new_config = """ 1177 | config = struct('lat_range', [""" + str(lat_min_map) + " " + str(lat_max_map) + """], 1178 | 'lon_range', [""" + str(lon_min_map) + " " + str(lon_max_map) + """], 1179 | 'known_location', struct('coord', [""" + selectedlat + " " + selectedlon + """], 1180 | 'name', '""" + selectedcity.rsplit(' (')[0].replace('_', ' ') + """'), 1181 | 'dir', 'png', 1182 | 'plot_kiwi', false, 1183 | 'plot_kiwi_json', """ + pkjn + """, 1184 | 'use_constraints', """ + uc + """, 1185 | 'new', """ + al + """ 1186 | );""" 1187 | new_mapbox = "lon, \" " + selectedlat + "\", \" " + selectedlon + "\", \" " + MAP_BOX + " \", \"iq" 1188 | dir_new = os.path.basename(os.path.dirname(mfile)) if os.path.basename( 1189 | os.path.dirname(mfile)) != dir_origin else dir_origin 1190 | # replace old config block by the new one 1191 | file_d = re.sub('(\n config = struct(.*)(\n(.*)){9})', new_config, file_d, flags=re.M) 1192 | file_d = re.sub(r'lon(.*)iq', new_mapbox, file_d) 1193 | file_d = file_d.replace("spectrogram.pdf", "temp.pdf") 1194 | file_d = file_d.replace(os.sep + dir_origin, os.sep + dir_new) 1195 | # get some file names and directory 1196 | logfile = os.path.basename(mfile).replace("empty", "txt") 1197 | tdoa_rootdir = up(up(up(mfile))) 1198 | newfile = tdoa_rootdir + os.sep + os.path.basename(mfile).replace("empty", "m") 1199 | # create the new proc_tdoa file 1200 | f = open(newfile, 'w') 1201 | f.write(file_d) 1202 | f.close() 1203 | # get only selected nodes for a TDoA run (PlotIQ mode 1) 1204 | PlotIQ(None, 1, file_list).run() 1205 | OctaveProcessing(os.path.basename(mfile).replace("empty", "m"), tdoa_rootdir, logfile).start() 1206 | self.compute_button.configure(text="Abort TDoA") 1207 | tdoa_in_progress = 1 1208 | 1209 | 1210 | class MainW(Tk, object): 1211 | """ Creating the Tk GUI design. """ 1212 | 1213 | def __init__(self): 1214 | Tk.__init__(self) 1215 | Tk.option_add(self, '*Dialog.msg.font', 'TkFixedFont 7') 1216 | self.gui = MainWindow(self) 1217 | 1218 | 1219 | def on_closing(): 1220 | """ Actions to perform when software is closed using the top-right check button. """ 1221 | try: # to kill octave 1222 | os.kill(PROC_PID, signal.SIGTERM) 1223 | except (NameError, OSError): 1224 | pass 1225 | os.kill(os.getpid(), signal.SIGTERM) 1226 | APP.destroy() 1227 | 1228 | 1229 | if __name__ == '__main__': 1230 | APP = MainW() 1231 | APP.title(VERSION + "| " + FREQUENCY + ALEID) 1232 | APP.protocol("WM_DELETE_WINDOW", on_closing) 1233 | APP.mainloop() 1234 | --------------------------------------------------------------------------------