PyFT8 2.8.0__tar.gz → 2.10.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. {pyft8-2.8.0 → pyft8-2.10.0}/PKG-INFO +39 -2
  2. pyft8-2.10.0/PyFT8/databases.py +311 -0
  3. {pyft8-2.8.0 → pyft8-2.10.0}/PyFT8/gui.py +4 -4
  4. pyft8-2.8.0/PyFT8/pskr_upload.py → pyft8-2.10.0/PyFT8/pskreporter.py +30 -5
  5. {pyft8-2.8.0 → pyft8-2.10.0}/PyFT8/pyft8.py +74 -138
  6. {pyft8-2.8.0 → pyft8-2.10.0}/PyFT8/receiver.py +1 -1
  7. {pyft8-2.8.0 → pyft8-2.10.0}/PyFT8/rigctrl.py +33 -3
  8. {pyft8-2.8.0 → pyft8-2.10.0}/PyFT8/transmitter.py +1 -1
  9. {pyft8-2.8.0 → pyft8-2.10.0}/PyFT8.egg-info/PKG-INFO +39 -2
  10. {pyft8-2.8.0 → pyft8-2.10.0}/PyFT8.egg-info/SOURCES.txt +2 -5
  11. {pyft8-2.8.0 → pyft8-2.10.0}/README.md +38 -1
  12. {pyft8-2.8.0 → pyft8-2.10.0}/pyproject.toml +1 -1
  13. pyft8-2.8.0/PyFT8/callhashes.py +0 -19
  14. pyft8-2.8.0/PyFT8/hamlib.py +0 -36
  15. pyft8-2.8.0/PyFT8/maidenhead.py +0 -32
  16. pyft8-2.8.0/PyFT8/mqtt.py +0 -141
  17. {pyft8-2.8.0 → pyft8-2.10.0}/LICENSE +0 -0
  18. {pyft8-2.8.0 → pyft8-2.10.0}/MANIFEST.in +0 -0
  19. {pyft8-2.8.0 → pyft8-2.10.0}/PyFT8/__init__.py +0 -0
  20. {pyft8-2.8.0 → pyft8-2.10.0}/PyFT8/time_utils.py +0 -0
  21. {pyft8-2.8.0 → pyft8-2.10.0}/PyFT8.egg-info/dependency_links.txt +0 -0
  22. {pyft8-2.8.0 → pyft8-2.10.0}/PyFT8.egg-info/entry_points.txt +0 -0
  23. {pyft8-2.8.0 → pyft8-2.10.0}/PyFT8.egg-info/requires.txt +0 -0
  24. {pyft8-2.8.0 → pyft8-2.10.0}/PyFT8.egg-info/top_level.txt +0 -0
  25. {pyft8-2.8.0 → pyft8-2.10.0}/setup.cfg +0 -0
  26. {pyft8-2.8.0 → pyft8-2.10.0}/tests/dev/CQ AAAA.py +0 -0
  27. {pyft8-2.8.0 → pyft8-2.10.0}/tests/dev/osd.py +0 -0
  28. {pyft8-2.8.0 → pyft8-2.10.0}/tests/dev/test_generate_wav.py +0 -0
  29. {pyft8-2.8.0 → pyft8-2.10.0}/tests/dev/test_loopback_performance.py +0 -0
  30. {pyft8-2.8.0 → pyft8-2.10.0}/tests/dev/view_worked_before.py +0 -0
  31. {pyft8-2.8.0 → pyft8-2.10.0}/tests/plot_baseline.py +0 -0
  32. {pyft8-2.8.0 → pyft8-2.10.0}/tests/spare.py +0 -0
  33. {pyft8-2.8.0 → pyft8-2.10.0}/tests/test_batch_and_live.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: PyFT8
3
- Version: 2.8.0
3
+ Version: 2.10.0
4
4
  Summary: FT8 Decoding and Encoding in Python with test/loopback code
5
5
  Author-email: G1OJS <g1ojs@yahoo.com>
6
6
  License-Expression: GPL-3.0-or-later
@@ -43,6 +43,7 @@ If you're interested in how this works, maybe have a look at [MiniPyFT8](https:/
43
43
  - Messages overlaid on waterfall signals that produce them
44
44
  - Worked-before info and fine grid locators / distance and bearing in the message boxes
45
45
  - List of stations hearing your transmissions on the selected band
46
+ - Marker on message if 2nd call is hearing you
46
47
  - Band activity in your level 4 square live updated next to band select buttons
47
48
  - Number of remote stations hearing your Tx, number of remote Txs that you're hearing, plus the same info for the 'best' station in your level 4 square
48
49
  - Data used for the above is cached to disk so is not lost when restarting the program
@@ -79,12 +80,48 @@ Once installed, you can use the following commands to run it. Otherwise, please
79
80
  | Launch configured GUI|pyft8 -i "Keyword1, Keyword2" -o "Keyword1, Keyword2" -c {config folder}| Config folder stores PyFT8.ini (your callsign, grid, buttons) and PyFT8.adi log file. Run this once to create default PyFT8.ini file.|
80
81
 
81
82
  ### Rig control
82
- I've included the Python code that I use with my Icom IC-7100 in the file 'rigctrl.py', and believe I've moved sufficient 'specification' for the rig protocol into the .ini file so that you can paste in your own rig specification (see for e.g. the Omnirig .ini file for your rig) and get it working with PyFT8 controlling PTT and frequency. I designed this code to drop the serial connection when it's not required, so that the rig's serial port can be accessed by other software at the same time.
83
+ I've included the Python code that I use with my Icom IC-7100 in the file 'rigctrl.py', and believe I've moved sufficient 'specification' for the rig protocol into the .ini file so that you can paste in your own rig specification (see for e.g. the Omnirig .ini file for your rig) and get it working with PyFT8 controlling PTT and frequency. I designed this code to drop the serial connection when it's not required, so that the rig's serial port can be accessed by other software at the same time (assuming that the other software returns the favour and doesn't permanently hog the serial port).
83
84
 
84
85
  I've also included a basic Hamlib interface which launches rigctld and uses that to control the rig. To use this, make sure that the hamlib section of the ini file is populated; this will then take precedence over the direct CAT control section.
85
86
 
86
87
  Alternatively, you can run PyFT8 without rig control; if there is no rig found, PyFT8 defaults to running without a rig connected. In this case, you need to provide your own PTT method and note that the band buttons will only set the information used for logging QSOs to the PyFT8.adi file. Or you can use PyFT8 as Rx-only.
87
88
 
89
+ ## Customising
90
+ Even if you don't want to edit the code, there's quite a bit that you can change in the .ini file. I haven't written a user guide yet, so here's my own .ini file for reference. I use CAT control so my hamlib block is disabled by adding a _ to the name of the block (I keep it there for testing only). I need to add some more switches really to catch up with recent additions.
91
+
92
+ ```
93
+ [station]
94
+ call = G1OJS
95
+ grid = IO90******
96
+ [gui]
97
+ loc = km_deg
98
+ wb = Y
99
+ [rig]
100
+ port = COM4
101
+ baud_rate = 9600
102
+ set_freq_command = FEFE88E0.05.0000000000.FD
103
+ set_freq_value = 5|5|vfBcdLU|1|0
104
+ ptt_on_command = FEFE88E0.1C00.01.FD
105
+ ptt_off_command = FEFE88E0.1C00.00.FD
106
+ [_hamlib_rig]
107
+ rigctld = C:/WSJT/wsjtx/bin/rigctld-wsjtx
108
+ port = COM4
109
+ baud_rate = 9600
110
+ model = 3070
111
+ [pskreporter]
112
+ upload = Y
113
+ [bands]
114
+ 160m = 1.840
115
+ 80m = 3.573
116
+ 60m = 5.357
117
+ 40m = 7.074
118
+ 20m = 14.074
119
+ 15m = 21.074
120
+ 10m = 28.074
121
+ 6m = 50.313
122
+ 2m = 144.174
123
+ ```
124
+
88
125
  ## Performance Compared with FT8_lib and WSJT-x
89
126
 
90
127
  The image below shows the number of decodes from PyFT8, WSJT-x V2.7.0 running in NORM mode, and FT8_lib, using the same 10 minutes of busy 20m audio that is used to test ft8_lib.
@@ -0,0 +1,311 @@
1
+ from PyFT8.pskreporter import PSKR_MQTT_listener
2
+ import threading, time, os, pickle
3
+
4
+ call_hashes = {}
5
+ def add_call_hashes(call):
6
+ global call_hashes
7
+ chars = " 0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ/"
8
+ call_padded = (call + " ")[:11]
9
+ hashes = []
10
+ for m in [10,12,22]:
11
+ x = 0
12
+ for c in call_padded:
13
+ x = 38*x + chars.find(c)
14
+ x = x & ((int(1) << 64) - 1)
15
+ x = x & ((1 << 64) - 1)
16
+ x = x * 47055833459
17
+ x = x & ((1 << 64) - 1)
18
+ x = x >> (64 - m)
19
+ hashes.append(x)
20
+ call_hashes[(x, m)] = call
21
+ return hashes
22
+
23
+ def grid_to_latlong(grid, centre = True):
24
+ lat, lon = -90, -180
25
+ grid = grid.upper()
26
+ if centre:
27
+ grid = grid + "LL44LL44LL44"[len(grid):]
28
+ mults = [20, 2, 2/24, 0.2/24, 0.2/(24*24), 0.02/(24*24)]
29
+ grid = grid[:2*len(mults)]
30
+ pairs = [grid[i:i+2] for i in range(0,len(grid),2)]
31
+ for i, p in enumerate(pairs):
32
+ zero = [ord('A'),ord('0')][i % 2]
33
+ lon += mults[i] * (ord(p[0]) - zero)
34
+ lat += mults[i] * (ord(p[1]) - zero) / 2
35
+ return (lat, lon)
36
+
37
+ def grids_to_dist_brg(sq1, sq2, units):
38
+ from numpy import sin, cos, asin, atan2, sqrt, radians, degrees
39
+ ll1, ll2 = grid_to_latlong(sq1), grid_to_latlong(sq2)
40
+ lats = [radians(ll1[0]), radians(ll2[0])]
41
+ dlat, dlon = radians(ll2[0] - ll1[0]), radians(ll2[1] - ll1[1])
42
+ s_lats, c_lats = sin(lats), cos(lats)
43
+ a = sin(dlat/2)**2 + c_lats[0] * c_lats[1] * sin(dlon/2)**2
44
+ r = 6371 * 2 * asin(sqrt(a))
45
+ b = atan2(c_lats[1] * sin(dlon), c_lats[0] * s_lats[1] - s_lats[0] * c_lats[1] * cos(dlon))
46
+ b *= (1.0 if 'km' in units else 0.621371)
47
+ return (r, degrees(b) % 360)
48
+
49
+ class DiskDict:
50
+ def __init__(self, file):
51
+ self.lock = threading.Lock()
52
+ self.file = file
53
+ self.data = {}
54
+ self.load()
55
+ threading.Thread(target = self._autosave, daemon = True).start()
56
+
57
+ def _autosave(self, autosave_period = 15):
58
+ while True:
59
+ time.sleep(autosave_period)
60
+ self.save()
61
+
62
+ def load(self):
63
+ with self.lock:
64
+ if(os.path.exists(self.file)):
65
+ with open(f"{self.file}","rb") as f:
66
+ self.data = pickle.load(f)
67
+
68
+ def save(self):
69
+ with self.lock:
70
+ tmp_file = f"{self.file}.tmp"
71
+ with open(tmp_file, "wb") as f:
72
+ pickle.dump(self.data, f)
73
+ f.flush()
74
+ os.fsync(f.fileno())
75
+ os.replace(tmp_file, self.file)
76
+
77
+ class History:
78
+ def __init__(self, config_folder, my_call, home_square, pskr_refresh_mins, parse_all_file):
79
+ self.pskr_refresh_mins = pskr_refresh_mins
80
+ self.config_folder = config_folder
81
+ self.my_call = my_call
82
+ self.home_square = home_square
83
+ self.home_square_lev4 = home_square[:4]
84
+ self.dist_brg_cache = {}
85
+ self.hearing_me = DiskDict(f"{self.config_folder}/hearing_me.pkl") # all-time record of hearing me
86
+ self.heard_by_me = DiskDict(f"{self.config_folder}/heard_by_me.pkl") # all-time record of heard by me
87
+ self.hearing_me_new = {}
88
+ self.heard_by_me_new = {}
89
+ self.call_to_grid = DiskDict(f"{self.config_folder}/call_to_grid.pkl") # all time cache call -> fine locator
90
+ self.band_TxRx_homecall_report_times = DiskDict(f"{self.config_folder}/report_times.pkl") # last 20 mins data -> per band tx/rx & current band detail
91
+ self.home_activity = {}
92
+ self.home_most_remotes = {}
93
+ self.lock = threading.Lock()
94
+ if parse_all_file:
95
+ self.load_all_file(f"{self.config_folder}/ALL.txt")
96
+ self.hearing_me.save()
97
+ self.heard_by_me.save()
98
+ print("All file parsed and saved to hearing_me / heard_by_me files")
99
+ else:
100
+ mqtt = PSKR_MQTT_listener(self.home_square_lev4, self.add_mqtt_spot)
101
+ threading.Thread(target = self.count_activity, daemon = True).start()
102
+
103
+ def band_from_MHz(self, fMHz): # rewrite this to use the band button defs from ini file
104
+ f = int(fMHz)
105
+ if f > 0:
106
+ freqs = [1,3,5,7,10,14,18,21,24,28,50,144,433]
107
+ idx = freqs.index(f) if f in freqs else -1
108
+ if idx > -1:
109
+ return ['160m','80m','60m','40m','30m','20m','17m','15m','12m','10m','6m','2m','70cm'][idx]
110
+
111
+ def load_from_wb(self, log_cache):
112
+ for key in log_cache:
113
+ key_parts = key.split('_')
114
+ if len(key_parts) > 1:
115
+ c, b, m = key_parts
116
+ if m == 'FT8':
117
+ self.add_myspots_record(self.hearing_me.data, None, b, c, 0, 0)
118
+ self.add_myspots_record(self.heard_by_me.data, None, b, c, 0, 0)
119
+
120
+ def load_all_file(self, all_file):
121
+ recs = self.parse_all_txt(all_file)
122
+ if not any(recs): return
123
+ for r in recs:
124
+ if r['md'] == 'FT8':
125
+ band = self.band_from_MHz(r['fMHz'])
126
+ if band is not None:
127
+ TxRx = 'Tx' if (r['TxRx'] == 'Tx' or r['call_b'] == self.my_call) else 'Rx'
128
+ call = r['call_b'] if TxRx == 'Rx' else r['call_a']
129
+ if len(call) > 3:
130
+ data = self.heard_by_me.data if TxRx == 'Rx' else self.hearing_me.data
131
+ self.add_myspots_record(data, None, band, call, 0, 0)
132
+
133
+ def parse_all_txt(self, all_file):
134
+ rows, recs = None, []
135
+ if os.path.exists(all_file):
136
+ with open(all_file, 'r') as f:
137
+ rows = f.readlines()
138
+ if rows is not None:
139
+ for r in rows:
140
+ fields = r.strip().split()
141
+ if len(fields) > 8:
142
+ call_a_pos = 7 if fields[7] != '~' else 8
143
+ recs.append({'fMHz':float(fields[1]), 'TxRx':fields[2], 'md':fields[3],
144
+ 'call_a':fields[call_a_pos], 'call_b':fields[call_a_pos + 1]} )
145
+ if not any(recs):
146
+ print("Didn't find any records in an ALL.txt file in the config folder")
147
+ return recs
148
+
149
+ def write_all_txt_row(self, cyclestart_string, fMHz, TxRx, mode, snr, dt, fHz, msg):
150
+ all_file = f"{self.config_folder}/ALL.txt"
151
+ filemode = 'w' if not os.path.exists(all_file) else 'a'
152
+ row = f"{cyclestart_string} {fMHz:8.3f} {TxRx} {mode} {snr:+03d} {dt:4.1f} {fHz:4.0f} {msg}"
153
+ with open(all_file, filemode) as f:
154
+ f.write(f"{row}\n")
155
+
156
+ def add_mqtt_spot(self, d):
157
+ tnow = int(time.time())
158
+ sc, rc = (d['sc'], d['sl']), (d['rc'], d['rl'])
159
+ for iTxRx, call_grid in enumerate([sc, rc]):
160
+ call, grid = call_grid
161
+ self.store_best_grid(call, grid)
162
+ if self.home_square_lev4 in grid:
163
+ self.add_homespots_record((d['b'], iTxRx, call), tnow)
164
+ if d['sc'] == self.my_call:
165
+ self.add_myspots_record(self.hearing_me.data, self.hearing_me_new, d['b'], d['rc'], tnow, d['rp'])
166
+ if d['rc'] == self.my_call:
167
+ self.add_myspots_record(self.heard_by_me.data, self.heard_by_me_new, d['b'], d['sc'], tnow, d['rp'])
168
+
169
+ def store_best_grid(self, call, grid):
170
+ if call.startswith('<'): return
171
+ existing_grid = self.call_to_grid.data.get(call, '')
172
+ if len(grid) > len(existing_grid):
173
+ self.call_to_grid.data[call] = grid
174
+
175
+ def add_homespots_record(self, key, t):
176
+ self.band_TxRx_homecall_report_times.data.setdefault(key, [])
177
+ self.band_TxRx_homecall_report_times.data[key].append(int(t))
178
+
179
+ def add_myspots_record(self, historic_data, new_alert_data, band, call, t, rp):
180
+ self._update_new_alert(band, call, historic_data, new_alert_data)
181
+ historic_data.setdefault(band, {})
182
+ if call in historic_data[band]:
183
+ if t < historic_data[band][call]['t']:
184
+ return
185
+ historic_data[band][call] = {'t': int(t),'rp':int(rp)}
186
+
187
+ def _update_new_alert(self, band, call, historic_data, new_alert_data):
188
+ if new_alert_data is None:
189
+ return
190
+ new = band not in historic_data
191
+ if not new:
192
+ new = call not in historic_data[band]
193
+ if new:
194
+ new_alert_data.setdefault(band, [])
195
+ new_alert_data[band].append(call)
196
+
197
+ def is_in_new_alert(self, band, call, new_alert_data):
198
+ result = False
199
+ if band in new_alert_data:
200
+ result = call in new_alert_data[band]
201
+ return result
202
+
203
+ def is_hearing_me(self, band, call, since_seconds):
204
+ result = False
205
+ if band in self.hearing_me.data:
206
+ if call in self.hearing_me.data[band]:
207
+ result = int(self.hearing_me.data[band][call]['t']) > since_seconds
208
+ return result
209
+
210
+ def count_activity(self):
211
+ import numpy as np
212
+ while True:
213
+ time.sleep(5)
214
+ self.home_activity = {}
215
+ self.home_most_remotes = {}
216
+ with self.lock:
217
+ # clear counters for each band
218
+ for b in self.home_activity:
219
+ self.home_activity[b] = [0, 0]
220
+ for b in self.home_most_remotes:
221
+ self.home_most_remotes[b] = [('',0), ('',0)]
222
+
223
+ # keep only the remote spots that happened in the self.pskr_refresh_mins window
224
+ for band_TxRx_homecall in self.band_TxRx_homecall_report_times.data:
225
+ band_TxRx_homecall_report_times = self.band_TxRx_homecall_report_times.data[band_TxRx_homecall]
226
+ band_TxRx_homecall_report_times = [t for t in band_TxRx_homecall_report_times if (time.time() - t) < 60*self.pskr_refresh_mins]
227
+ self.band_TxRx_homecall_report_times.data[band_TxRx_homecall] = band_TxRx_homecall_report_times
228
+
229
+ # count number of local Tx and Rx, and identify the local Tx and Rx with most remote spots
230
+ for band_TxRx_homecall in self.band_TxRx_homecall_report_times.data:
231
+ band_TxRx_homecall_report_times = self.band_TxRx_homecall_report_times.data[band_TxRx_homecall]
232
+ if len(band_TxRx_homecall_report_times):
233
+ b, iTxRx, c = band_TxRx_homecall
234
+ self.home_activity.setdefault(b, [0, 0])
235
+ self.home_activity[b][iTxRx] +=1
236
+ self.home_most_remotes.setdefault(b, [('',0), ('',0)])
237
+ nremotes = len(band_TxRx_homecall_report_times)
238
+ current_winner = self.home_most_remotes[b][iTxRx]
239
+ if nremotes > current_winner[1]:
240
+ if c != self.my_call:
241
+ self.home_most_remotes[b][iTxRx] = (c, nremotes)
242
+
243
+ def get_spot_counts(self, band, call):
244
+ tx_reports = self.band_TxRx_homecall_report_times.data.get((band, 0, call), [])
245
+ rx_reports = self.band_TxRx_homecall_report_times.data.get((band, 1, call), [])
246
+ n_spotting = len(tx_reports) if tx_reports else 0
247
+ n_spotted = len(rx_reports) if rx_reports else 0
248
+ return n_spotted, n_spotting
249
+
250
+ def get_dist_brg(self, grid, units):
251
+ self.dist_brg_cache.setdefault(grid, grids_to_dist_brg(self.home_square, grid, units))
252
+ return self.dist_brg_cache[grid]
253
+
254
+ def get_geo_text(self, call, units):
255
+ geo_text = ''
256
+ grid = self.call_to_grid.data.get(call, False)
257
+ if grid:
258
+ loc = grid if units == 'grid' else self.get_dist_brg(grid, units)
259
+ units_str = '' if units == 'grid' else ('km' if 'km' in units else 'mi')
260
+ geo_text = f"{int(loc[0]):5d}{units_str} {int(loc[1]):3d}°"
261
+ return geo_text
262
+
263
+ def ensure_file_exists(path, header = None):
264
+ try:
265
+ with open(path, "x") as f:
266
+ if header is not None:
267
+ f.write(header)
268
+ except FileExistsError:
269
+ pass
270
+
271
+ class ADIF:
272
+ def __init__(self, logfile):
273
+ self.adif_log_file = logfile
274
+ ensure_file_exists(self.adif_log_file, header = "header <eoh>\n")
275
+ self.cache = self._build_cache()
276
+
277
+ def log(self, times, band_info, mStation, oStation, rpts):
278
+ log_dict = {'call':oStation['c'], 'gridsquare':oStation['g'], 'mode':'FT8',
279
+ 'operator':mStation['c'], 'station_callsign':mStation['c'], 'my_gridsquare':mStation['g'],
280
+ 'rst_sent':rpts['sent'], 'rst_rcvd':rpts['rcvd'],
281
+ 'qso_date':time.strftime("%Y%m%d", times['time_on']), 'qso_date_off':time.strftime("%Y%m%d", times['time_off']),
282
+ 'time_on':time.strftime("%H%M%S", times['time_on']), 'time_off':time.strftime("%H%M%S", times['time_on']),
283
+ 'band':band_info['b'], 'freq':band_info['fMHz']}
284
+ with open(self.adif_log_file,'a') as f:
285
+ for k, v in log_dict.items():
286
+ v = str(v)
287
+ f.write(f"<{k}:{len(v)}>{v} ")
288
+ f.write(f"<eor>\n")
289
+ cbm = log_dict['call'] + "_" + log_dict['band'] + "_FT8"
290
+ tm = time.time()
291
+ self.cache[log_dict['call']] = tm
292
+ self.cache[cbm] = tm
293
+
294
+ def _build_cache(self):
295
+ import calendar
296
+ def parse(rec, field):
297
+ p = rec.find(field)
298
+ if p > 0:
299
+ p1, p2 = rec.find(':',p), rec.find('>',p)
300
+ n = int(rec[p1+1:p2])
301
+ return rec[p2+1: p2+1+n]
302
+ cache = {}
303
+ with open(self.adif_log_file, 'r') as f:
304
+ for l in f.readlines():
305
+ if parse(l, 'mode') == "FT8":
306
+ c, b, d, t = parse(l, 'call'), parse(l, 'band'), parse(l, 'qso_date'), parse(l, 'time_on')
307
+ time_tuple = time.strptime(d+t, "%Y%m%d%H%M%S")
308
+ tm = calendar.timegm(time_tuple)
309
+ cache[c] = tm
310
+ cache[c + "_"+b+"_FT8"] = tm
311
+ return cache
@@ -14,7 +14,7 @@ HOVERCOLOR = 'darkgreen'
14
14
  ACTIVE_BUTTON_COLOR = 'cyan'
15
15
  INACTIVE_BUTTON_COLOR = '#edeef0'
16
16
  MAX_FONT_SIZE_MAIN = 10
17
- L = {'pmargin':0.04, 'sidebar_width': 0.16, 'banner_height':0.1, 'vsep1':0.01, 'hsep1':0.02}
17
+ L = {'pmargin':0.04, 'sidebar_width': 0.17, 'banner_height':0.1, 'vsep1':0.01, 'hsep1':0.02}
18
18
 
19
19
  # ================== WATERFALL ======================================================
20
20
 
@@ -31,7 +31,7 @@ class Scrollbox:
31
31
  self.lineartists = []
32
32
  for i in range(self.nlines):
33
33
  self.lineartists.append(self.ax.text(0.03,1 - self.line_height * (i+1),
34
- '', color = MAIN_TEXT_COLOR, fontsize = self.fontsize))
34
+ '', color = MAIN_TEXT_COLOR, fontsize = self.fontsize, clip_on = True))
35
35
  if monospace:
36
36
  self.lineartists[-1].set_fontfamily('monospace')
37
37
  self.ax.set_xticks([])
@@ -67,7 +67,7 @@ class Msg_box:
67
67
  self.onclick = onclick
68
68
  rect = Rectangle((tbin, fbin), width=w, height=h, alpha=0.6, edgecolor='lime', lw=2)
69
69
  self.patch = ax.add_patch(rect)
70
- self.text_inst = ax.text(tbin, fbin+2, '', fontsize='small', fontweight='bold' )
70
+ self.text_inst = ax.text(tbin, fbin+2, '', fontsize='small', fontweight='bold', clip_on = True )
71
71
  self.cid = fig.canvas.mpl_connect('button_press_event', self._onclick)
72
72
  self.expire = 0
73
73
 
@@ -109,7 +109,7 @@ class ButtonBox:
109
109
  self.info_axs.set_xticks([])
110
110
  self.info_axs.set_yticks([])
111
111
  self.info_axs.set_facecolor(TEXT_BACKGROUND_COLOR)
112
- self.label2 = self.info_axs.text(0.03, 0.5, '', color = INFO_TEXT_COLOR, verticalalignment = 'center')
112
+ self.label2 = self.info_axs.text(0.03, 0.5, '', color = INFO_TEXT_COLOR, verticalalignment = 'center', clip_on = True)
113
113
  self.btn_widg = Button(self.btn_axs, btn_label, color = BUTTONCOLOR, hovercolor = HOVERCOLOR)
114
114
  self.label = self.btn_widg.label
115
115
  self.label.set_color(MAIN_TEXT_COLOR)
@@ -1,11 +1,36 @@
1
- import socket
2
- import struct
3
- import time
4
- import threading
5
- import random
1
+ import paho.mqtt.client as mqtt
2
+ from ast import literal_eval
3
+ import socket, struct, random
4
+ import threading, time
6
5
 
7
6
  MAX_REPORTS = 90
8
7
 
8
+ class PSKR_MQTT_listener:
9
+ def __init__(self, home_square, on_spot):
10
+ self.home_square = home_square
11
+ self.on_spot = on_spot
12
+ mqttc = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2)
13
+ mqttc.on_connect = self.on_connect
14
+ mqttc.on_message = self.on_message
15
+ try:
16
+ mqttc.connect("mqtt.pskreporter.info", 1883, 60)
17
+ except:
18
+ print("[MQTT] connection error")
19
+ threading.Thread(target = mqttc.loop_forever, daemon = True).start()
20
+
21
+ def on_connect(self, client, userdata, flags, reason_code, properties):
22
+ #pskr/filter/v2/{band}/{mode}/{sendercall}/{receivercall}/{senderlocator}/{receiverlocator}/{sendercouniTxRxy}/{receivercouniTxRxy}
23
+ print(f"[MQTT] Requesting mqtt feed for {self.home_square}")
24
+ client.subscribe(f"pskr/filter/v2/+/FT8/+/+/{self.home_square}/#")
25
+ client.subscribe(f"pskr/filter/v2/+/FT8/+/+/+/{self.home_square}/#")
26
+
27
+ def on_message(self, client, userdata, msg):
28
+ try:
29
+ d = literal_eval(msg.payload.decode())
30
+ except:
31
+ return
32
+ self.on_spot(d)
33
+
9
34
  class PSKR_upload:
10
35
  # https://pskreporter.info/pskdev.html
11
36
  # https://pskreporter.info/cgi-bin/psk-analysis.pl