├── 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 |
6 |
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 |
10 |
11 |
--------------------------------------------------------------------------------
/.idea/directTDoA4.iml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
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 | 
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 |
--------------------------------------------------------------------------------