PyFT8 3.6.2__tar.gz → 3.6.4__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 (44) hide show
  1. {pyft8-3.6.2 → pyft8-3.6.4}/PKG-INFO +4 -4
  2. pyft8-3.6.4/PyFT8/gui.py +311 -0
  3. {pyft8-3.6.2 → pyft8-3.6.4}/PyFT8/message_broker.py +5 -8
  4. {pyft8-3.6.2 → pyft8-3.6.4}/PyFT8/pyft8.py +6 -5
  5. {pyft8-3.6.2 → pyft8-3.6.4}/PyFT8/qso_manager.py +1 -1
  6. {pyft8-3.6.2 → pyft8-3.6.4}/PyFT8/receiver.py +7 -0
  7. {pyft8-3.6.2 → pyft8-3.6.4}/PyFT8.egg-info/PKG-INFO +4 -4
  8. {pyft8-3.6.2 → pyft8-3.6.4}/PyFT8.egg-info/SOURCES.txt +3 -1
  9. {pyft8-3.6.2 → pyft8-3.6.4}/README.md +3 -3
  10. {pyft8-3.6.2 → pyft8-3.6.4}/pyproject.toml +1 -1
  11. pyft8-3.6.4/tests/play_wavs.py +54 -0
  12. pyft8-3.6.4/tests/plot_decode_times.py +26 -0
  13. {pyft8-3.6.2 → pyft8-3.6.4}/tests/spare.py +5 -5
  14. pyft8-3.6.4/tests/spare2.py +28 -0
  15. {pyft8-3.6.2 → pyft8-3.6.4}/tests/test_batch_and_live.py +14 -5
  16. pyft8-3.6.2/PyFT8/gui.py +0 -298
  17. pyft8-3.6.2/tests/plot_baseline.py +0 -38
  18. {pyft8-3.6.2 → pyft8-3.6.4}/LICENSE +0 -0
  19. {pyft8-3.6.2 → pyft8-3.6.4}/MANIFEST.in +0 -0
  20. {pyft8-3.6.2 → pyft8-3.6.4}/PyFT8/__init__.py +0 -0
  21. {pyft8-3.6.2 → pyft8-3.6.4}/PyFT8/databases.py +0 -0
  22. {pyft8-3.6.2 → pyft8-3.6.4}/PyFT8/osd.py +0 -0
  23. {pyft8-3.6.2 → pyft8-3.6.4}/PyFT8/pskreporter.py +0 -0
  24. {pyft8-3.6.2 → pyft8-3.6.4}/PyFT8/rigctrl.py +0 -0
  25. {pyft8-3.6.2 → pyft8-3.6.4}/PyFT8/time_utils.py +0 -0
  26. {pyft8-3.6.2 → pyft8-3.6.4}/PyFT8/transmitter.py +0 -0
  27. {pyft8-3.6.2 → pyft8-3.6.4}/PyFT8.egg-info/dependency_links.txt +0 -0
  28. {pyft8-3.6.2 → pyft8-3.6.4}/PyFT8.egg-info/entry_points.txt +0 -0
  29. {pyft8-3.6.2 → pyft8-3.6.4}/PyFT8.egg-info/requires.txt +0 -0
  30. {pyft8-3.6.2 → pyft8-3.6.4}/PyFT8.egg-info/top_level.txt +0 -0
  31. {pyft8-3.6.2 → pyft8-3.6.4}/setup.cfg +0 -0
  32. {pyft8-3.6.2 → pyft8-3.6.4}/tests/misc/CQ AAAA.py +0 -0
  33. {pyft8-3.6.2 → pyft8-3.6.4}/tests/misc/animation.py +0 -0
  34. {pyft8-3.6.2 → pyft8-3.6.4}/tests/misc/bits_loopback.py +0 -0
  35. {pyft8-3.6.2 → pyft8-3.6.4}/tests/misc/bits_loopback_short.py +0 -0
  36. {pyft8-3.6.2 → pyft8-3.6.4}/tests/misc/osd.py +0 -0
  37. {pyft8-3.6.2 → pyft8-3.6.4}/tests/misc/symbols_loopback.py +0 -0
  38. {pyft8-3.6.2 → pyft8-3.6.4}/tests/misc/test_generate_wav.py +0 -0
  39. {pyft8-3.6.2 → pyft8-3.6.4}/tests/misc/test_loopback_performance.py +0 -0
  40. {pyft8-3.6.2 → pyft8-3.6.4}/tests/misc/text.py +0 -0
  41. {pyft8-3.6.2 → pyft8-3.6.4}/tests/misc/view_worked_before.py +0 -0
  42. {pyft8-3.6.2 → pyft8-3.6.4}/tests/pipeline/Fine tuning compare IOS_PC.py +0 -0
  43. {pyft8-3.6.2 → pyft8-3.6.4}/tests/pipeline/Targetted tests.py +0 -0
  44. {pyft8-3.6.2 → pyft8-3.6.4}/tests/pipeline/test_01.wav +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: PyFT8
3
- Version: 3.6.2
3
+ Version: 3.6.4
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
@@ -110,11 +110,11 @@ upload = Y
110
110
  2m = 144.174
111
111
  ```
112
112
 
113
- ## Performance Compared with FT8_lib and WSJT-x
113
+ ## Performance Compared with WSJT-x
114
114
 
115
- 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.
115
+ The image below shows the number of decodes from PyFT8, WSJT-x V2.7.0 running in NORM mode, using the same 10 minutes of busy 20m audio that is used to test ft8_lib. On this dataset, WSJT-x performs almost the same on DEEP mode as it does on NORM, and both beat PyFT8. However, PyFT8 gets very close to WSJT-x's performance on FAST mode.
116
116
 
117
- <img width="900" height="898" alt="batch test performance" src="https://github.com/user-attachments/assets/513d4121-5483-4e7b-81df-785dc4db3d37" />
117
+ <img width="900" height="900" alt="live playback vs wsjtx_v2 7 0" src="https://github.com/user-attachments/assets/853e98b2-5b9b-4241-b5f3-181fe193642e" />
118
118
 
119
119
  ## Limitations
120
120
  PyFT8 doesn't decode / encode *all* message types. The table below shows which are handled.
@@ -0,0 +1,311 @@
1
+ import numpy as np
2
+ import matplotlib.pyplot as plt
3
+ from matplotlib import rcParams
4
+ import queue
5
+ from matplotlib.widgets import Button
6
+ from PyFT8.time_utils import time_utils
7
+
8
+ rcParams['toolbar'] = 'None'
9
+ MAIN_TEXT_COLOR = '#f0f9fa'
10
+ TEXT_BACKGROUND_COLOR = '#2a2b2b'
11
+ INFO_TEXT_COLOR = 'white'
12
+ BUTTONCOLOR = 'grey'
13
+ HOVERCOLOR = 'darkgreen'
14
+ ACTIVE_BUTTON_COLOR = 'cyan'
15
+ INACTIVE_BUTTON_COLOR = '#edeef0'
16
+ MAX_FONT_SIZE_MAIN = 10
17
+ L = {'pmargin':0.04, 'sidebar_width': 0.17, 'banner_height':0.1, 'vsep1':0.01, 'hsep1':0.02}
18
+
19
+ def add_my_axes(fig, pos):
20
+ ax = fig.add_axes(pos)
21
+ ax.set_xticks([])
22
+ ax.set_yticks([])
23
+ ax.set_facecolor(TEXT_BACKGROUND_COLOR)
24
+ return ax
25
+
26
+ MESSAGE_TYPES = {'generic':{'bg':'blue', 'fg':'white', 'alpha':0.5}, 'CQ':{'bg':'green', 'fg':'white', 'alpha':0.8},
27
+ 'from_me': {'bg':'yellow', 'fg':'black', 'alpha':0.95}, 'to_me':{'bg':'red', 'fg':'white', 'alpha':0.9}}
28
+ class Msg_box:
29
+ def __init__(self, fig, ax, x, y, w, h, message):
30
+ from matplotlib.patches import Rectangle
31
+ self.fig, self.ax = fig, ax
32
+ message_type_params = MESSAGE_TYPES[message['message_type']]
33
+ rect = Rectangle((x, y), w, h, edgecolor='lime', lw=2)
34
+ self.patch = self.ax.add_patch(rect)
35
+ self.patch.set_visible(False)
36
+ text_inst = self.ax.text(x, y+1, message['display_text'], fontsize='small', fontweight = 'bold' )
37
+ text_inst.set_visible(False)
38
+ text_inst.set_color(message_type_params['fg'])
39
+ self.patch.set_facecolor(message_type_params['bg'])
40
+ self.patch.set_alpha(message_type_params['alpha'])
41
+ self.message = message
42
+ self.cycle = message['their_tx_cycle']
43
+ self.artists = [self.patch, text_inst]
44
+ for a in self.artists:
45
+ a.set_visible(True)
46
+
47
+ def contains(self, x, y):
48
+ if self.patch.get_visible():
49
+ return self.patch.get_extents().contains(x, y)
50
+
51
+ def draw(self):
52
+ for a in self.artists:
53
+ self.ax.draw_artist(a)
54
+
55
+ def hide(self):
56
+ for a in self.artists:
57
+ a.set_visible(False)
58
+
59
+ def remove(self):
60
+ for a in self.artists:
61
+ a.set_visible(False)
62
+ a.remove()
63
+ self.artists.remove(a)
64
+ self = None
65
+
66
+ class Panel:
67
+ def __init__(self, fig, pos):
68
+ self.fig = fig
69
+ self.ax = self.fig.add_axes(pos)
70
+ self.ax.set_xticks([])
71
+ self.ax.set_yticks([])
72
+ self.ax.set_facecolor(TEXT_BACKGROUND_COLOR)
73
+ box_h = self.ax.get_window_extent().height
74
+ font_h = 10
75
+ self.nlines = int(0.5*box_h / font_h)
76
+ self.artists = []
77
+
78
+ def clear(self):
79
+ for a in self.artists:
80
+ a.set_visible(False)
81
+ a.remove()
82
+ self.artists = [a for a in self.artists if a.get_visible()]
83
+
84
+ def print_row(self, row_text, row_number, color = None):
85
+ a = self.ax.text(0.03, 0.03+(self.nlines - row_number)/self.nlines, row_text)
86
+ a.set_fontfamily('monospace')
87
+ if color is not None:
88
+ a.set_color(color)
89
+ self.artists.append(a)
90
+ self.ax.draw_artist(a)
91
+
92
+ class ButtonBox:
93
+ def __init__(self, fig, box, btn_pc = 30, btn_text = ' ', info_text = ' '):
94
+ self.id = btn_text
95
+ btnbox, infobox = box.copy(), box.copy()
96
+ btnbox[2] = box[2] * btn_pc /100
97
+ infobox[2] = box[2] * (100-btn_pc) /100
98
+ infobox[0] = box[0] + box[2] * (btn_pc /100)
99
+ self.btn_axs = add_my_axes(fig, btnbox)
100
+ self.btn_axs.set_facecolor(BUTTONCOLOR)
101
+ self.bbox = self.btn_axs.bbox
102
+ self.btn_widg = Button(self.btn_axs, btn_text, color = BUTTONCOLOR, hovercolor = HOVERCOLOR)
103
+ self.info_axs = add_my_axes(fig, infobox)
104
+ self.info_art = self.info_axs.text(0.03, 0.5, info_text, color = INFO_TEXT_COLOR, verticalalignment = 'center', clip_on = True)
105
+ self.state_is_active = None
106
+ self.set_state(False)
107
+
108
+ def set_state(self, is_active: bool):
109
+ if is_active != self.state_is_active:
110
+ self.state_is_active = is_active
111
+ color = ACTIVE_BUTTON_COLOR if is_active else INACTIVE_BUTTON_COLOR
112
+ self.btn_widg.label.set_color(color)
113
+ self.set_info_text(info_text = None, color = color)
114
+
115
+ def set_info_text(self, info_text, color = None):
116
+ if info_text is not None:
117
+ self.info_art.set_text(info_text)
118
+ if color is not None:
119
+ self.info_art.set_color(color)
120
+
121
+ class Gui:
122
+ def __init__(self, message_broker, rig_control, console_print, configured_bands, hearing_me_since_mins = 5):
123
+ self.hearing_me_since_mins = hearing_me_since_mins
124
+ self.waterfall_data = message_broker.waterfall_data
125
+ self.history = message_broker.history
126
+ self.qso_manager = None
127
+ self.configured_bands = configured_bands
128
+ self.console_print = console_print
129
+ self.myCall, self.myGrid = message_broker.myCall, message_broker.myGrid
130
+ self.band_info = {'current_band': None, 'fMHz':0, 'time_set':0}
131
+
132
+ self.plt = plt
133
+ self.fig = plt.figure(figsize = (10,10), facecolor=(.18, .71, .71, 0.4))
134
+ self.fig.canvas.manager.set_window_title('PyFT8 by G1OJS')
135
+ self.wf_top = 1-L['pmargin']-L['banner_height']-L['vsep1']
136
+ self.wf_left = L['pmargin']+L['sidebar_width']+L['hsep1']
137
+ self.needs_redraw = False
138
+
139
+ self.ax_wf = add_my_axes(self.fig, [self.wf_left, L['pmargin'], 1-self.wf_left-L['pmargin'], self.wf_top-L['pmargin']])
140
+ self.image = self.ax_wf.imshow(self.waterfall_data['data'],vmax=120,vmin=90,origin='lower',interpolation='none', aspect = 'auto')
141
+
142
+ self.home_panel = Panel(self.fig, [L['pmargin'], self.wf_top+L['vsep1'], L['sidebar_width'], L['banner_height']])
143
+ ax = self.home_panel.ax
144
+ ax.draw_artist(ax.text(-0.15,0.75,'Tx'))
145
+ ax.draw_artist(ax.text(-0.15,0.125,'Rx'))
146
+
147
+ self.hearing_page = 0
148
+ self.msg_boxes = []
149
+ self.button_boxes = []
150
+
151
+ bh, bsep = 0.02, 0.002
152
+ bx, bw = L['pmargin'], L['sidebar_width']
153
+ bxs = self.button_boxes
154
+ self.button_boxes.append(ButtonBox(self.fig, [bx, self.wf_top - (len(bxs)+1)*bh + bsep, bw, bh-bsep], btn_text = "CQ", btn_pc = 100) )
155
+ self.button_boxes.append(ButtonBox(self.fig, [bx, self.wf_top - (len(bxs)+1)*bh + bsep, bw, bh-bsep], btn_text = "Repeat last", btn_pc = 100) )
156
+ self.button_boxes.append(ButtonBox(self.fig, [bx, self.wf_top - (len(bxs)+1)*bh + bsep, bw, bh-bsep], btn_text = "Tx off", btn_pc = 100) )
157
+ for band in self.configured_bands:
158
+ self.button_boxes.append(ButtonBox(self.fig, [bx, self.wf_top - (len(self.button_boxes)+1) * bh + bsep, bw, bh-bsep], btn_pc = 30, btn_text = band))
159
+
160
+ hm_top = self.wf_top - (len(self.button_boxes)+2) * bh + bsep - L['vsep1']
161
+
162
+ self.console = Panel(self.fig, [self.wf_left, self.wf_top+L['vsep1'], 1-self.wf_left-L['pmargin'], L['banner_height']])
163
+ self.console_rows_text = None
164
+ self.hearing_panel = Panel(self.fig, [L['pmargin'], L['pmargin'], L['sidebar_width'], hm_top])
165
+
166
+ self.cid = self.fig.canvas.mpl_connect('button_press_event', self._oncanvasclick)
167
+
168
+ def main_loop(self):
169
+ last_ptr = 0
170
+ self.plt.show(block = False)
171
+ while True:
172
+ self.image.set_data(self.waterfall_data['data'])
173
+ if self.needs_redraw:
174
+ self.needs_redraw = False
175
+ plt.pause(0.01)
176
+ else:
177
+ self.ax_wf.draw_artist(self.image)
178
+ for mb in self.msg_boxes:
179
+ mb.draw()
180
+ self.fig.canvas.update()
181
+ self.fig.canvas.flush_events()
182
+
183
+ def before_search(self, curr_cycle):
184
+ self._hide_msg_boxes(curr_cycle)
185
+
186
+ def after_search(self,curr_cycle):
187
+ self._refresh_hearing()
188
+ self._refresh_band_buttons()
189
+ self._refresh_home_panel()
190
+ self._clear_msg_boxes(curr_cycle)
191
+
192
+ def display_message(self, message):
193
+ x = int(message['t0'] / self.waterfall_data['dt'] + message['their_tx_cycle'] * self.waterfall_data['pixels_per_cycle'])
194
+ y = int(message['fHz'] / self.waterfall_data['df'])
195
+ mb = Msg_box(self.fig, self.ax_wf, x, y, self.waterfall_data['sig_w'], self.waterfall_data['sig_h'], message)
196
+ self.msg_boxes.append(mb)
197
+
198
+ def get_band_info(self):
199
+ return self.band_info
200
+
201
+ def register_qso_manager(self, qsm):
202
+ self.qso_manager = qsm
203
+
204
+ def set_bandstats_title(self, txt):
205
+ self.home_panel.ax.set_title(txt, fontsize = 10)
206
+
207
+ def update_console(self, text, color):
208
+ if self.console_rows_text is None:
209
+ self.console_rows_text = [{'text':'','color':'white'} for i in range(self.console.nlines)]
210
+ self.console_rows_text[1:] = self.console_rows_text[:-1]
211
+ self.console_rows_text[0] = {'text':text,'color':color}
212
+ self.console.clear()
213
+ for row_number, row_text in enumerate(self.console_rows_text):
214
+ self.console.print_row(row_text['text'], self.console.nlines - row_number,
215
+ color = row_text['color'])
216
+ self.needs_redraw = True
217
+
218
+ def _set_band(self, band):
219
+ if band in self.configured_bands:
220
+ self.band_info = {'current_band':band, 'fMHz':self.configured_bands[band], 'time_set':time_utils.time()}
221
+ self.console_print(f"[PyFT8] Set band: {self.band_info['current_band']} {self.band_info['fMHz']}")
222
+
223
+ def _oncanvasclick(self, clickargs):
224
+ #print(f"Click at {time_utils.cycle_time():6.2f}")
225
+
226
+ if clickargs.inaxes is self.ax_wf:
227
+ for mb in self.msg_boxes:
228
+ if mb.contains(clickargs.x, clickargs.y):
229
+ self.console_print(f"[GUI] Clicked on message '{mb.message['msg_text']}'")
230
+ self.qso_manager.on_click({'action':"MESSAGE_CLICK", 'message':mb.message})
231
+ return
232
+ for bb in self.button_boxes:
233
+ if bb.bbox.contains(clickargs.x, clickargs.y):
234
+ if bb.id[:-1].isnumeric():
235
+ self.needs_redraw = True
236
+ self._set_band(bb.id)
237
+ self.qso_manager.on_click({'action':"SET_BAND", 'fMHz':self.configured_bands[bb.id]})
238
+ self.after_search(curr_cycle = None)
239
+ else:
240
+ action = ["CQ", "RPT_LAST", "TX_OFF"][['CQ', 'Repeat last', 'Tx off'].index(bb.id)]
241
+ self.qso_manager.on_click({'action':action})
242
+
243
+ def _hide_msg_boxes(self, curr_cycle):
244
+ to_hide = [mb for mb in self.msg_boxes if mb.cycle == curr_cycle]
245
+ for mb in to_hide:
246
+ mb.hide()
247
+ self.needs_redraw = True
248
+
249
+ def _clear_msg_boxes(self, curr_cycle = None):
250
+ to_remove = [mb for mb in self.msg_boxes if mb.cycle == curr_cycle or curr_cycle is None]
251
+ self.msg_boxes = [mb for mb in self.msg_boxes if mb.cycle != curr_cycle]
252
+ for mb in to_remove:
253
+ mb.remove()
254
+ self.needs_redraw = True
255
+
256
+ def _text_row(self, ax, x, y, text = '', color = 'white', **args):
257
+ art = ax.text(x, y, text, color = color)
258
+ art.set_fontfamily('monospace')
259
+ return art
260
+
261
+ def _refresh_home_panel(self):
262
+ current_band = self.band_info['current_band']
263
+ if current_band is not None and self.history is not None:
264
+ if current_band in self.history.home_most_remotes:
265
+ tx_lead, rx_lead = self.history.home_most_remotes[current_band]
266
+ n_spotted, n_spotting = self.history.get_spot_counts(current_band, self.myCall)
267
+ self.home_panel.clear()
268
+ self.home_panel.print_row(f"{self.myCall:<7} {tx_lead[0]:<7}", 1, color = '#ff756b' )
269
+ self.home_panel.print_row(f"{n_spotting:<7} {tx_lead[1]:<7}", 2, color = '#ff756b' )
270
+ self.home_panel.print_row(f"{self.myCall:<7} {rx_lead[0]:<7}", 4, color = '#b6f0c6' )
271
+ self.home_panel.print_row(f"{n_spotted:<7} {rx_lead[1]:<7}", 5, color = '#b6f0c6' )
272
+
273
+ def _refresh_band_buttons(self):
274
+ current_band = self.band_info['current_band']
275
+ grd = self.myGrid[:4]
276
+ for bb in self.button_boxes:
277
+ bb.set_state(bb.id == current_band)
278
+ if bb.id in self.history.home_activity:
279
+ cnts = self.history.home_activity[bb.id]
280
+ bb.set_info_text(f"{cnts[0]}Tx, {cnts[1]}Rx")
281
+ self.needs_redraw = True
282
+
283
+ def _refresh_hearing(self):
284
+ current_band = self.band_info['current_band']
285
+ if current_band is not None and self.history is not None:
286
+ historic_data = self.history.hearing_me.data if self.hearing_page == 1 else self.history.heard_by_me.data
287
+ new_calls_data = self.history.hearing_me_new if self.hearing_page == 1 else self.history.heard_by_me_new
288
+ timewindow_str = f"<{self.hearing_me_since_mins:.0f} mins"
289
+ title_txt = f"Hearing me {timewindow_str}" if self.hearing_page==1 else f"Heard by me {timewindow_str}"
290
+ self.hearing_panel.clear()
291
+ self.hearing_panel.print_row(title_txt, 2, color = 'white')
292
+ if current_band in historic_data:
293
+ tnow = time_utils.time()
294
+ band_rpts = historic_data[current_band]
295
+ calls_now = [call for call in band_rpts if (tnow - band_rpts[call]['t']) < 60*self.hearing_me_since_mins]
296
+ calls_now.sort(key = lambda c: band_rpts[c]['t'], reverse = True)
297
+ subtitle_txt = f"{len(calls_now)}/{len(band_rpts)} now/ever"
298
+ self.hearing_panel.print_row(subtitle_txt, 3, color = 'white')
299
+ for i, remote_call in enumerate(calls_now[:self.hearing_panel.nlines - 3]):
300
+ rpt = band_rpts[remote_call]
301
+ row_txt = f"{remote_call:<7} {self.history.get_geo_text(remote_call)}"
302
+ color = 'white' if self.history.is_in_new_alert(current_band, remote_call, new_calls_data) else 'lime'
303
+ self.hearing_panel.print_row(row_txt, i+4, color = color)
304
+ self.hearing_page = (self.hearing_page +1 )%2
305
+
306
+
307
+
308
+
309
+
310
+
311
+
@@ -22,28 +22,25 @@ class Broker():
22
22
  def process_message(self, message_dict):
23
23
  hail, their_call, grid_rpt = message_dict['hail'], message_dict['their_call'], message_dict['grid_rpt']
24
24
  cyclestart_string, their_snr = message_dict['cyclestart_string'], message_dict['their_snr']
25
- m = message_dict
26
- screen_format = f"{cyclestart_string} {their_snr} {m['dt']:4.1f} {m['fHz']:4.0f} ~ {hail} {their_call} {grid_rpt}"
27
- print(f"{screen_format:50s} decoded@ {m['decode_completed']:5.1f}s, dec = {m['decode_status']}")
28
-
29
25
  mtype_val = 0 + 1*(their_call == self.myCall) + 2*(hail == self.myCall) + 3*(their_call != self.myCall and hail.startswith('CQ'))
30
26
  mtype = ['generic', 'from_me', 'to_me', 'CQ'][mtype_val]
31
27
  message_dict.update( {'message_type':mtype, 'display_text':f"{hail} {their_call} {grid_rpt}",
32
28
  'priority':(mtype == 'to_me' or mtype == 'CQ')} )
33
-
34
29
  if self.history:
35
30
  current_band = self.gui.get_band_info()['current_band']
36
31
  hearing_me, wb_text, geo_text = self.history.get_message_extra_info(their_call, current_band)
37
32
  display_text = f"{message_dict['hail']} {message_dict['their_call']} {message_dict['grid_rpt']} {hearing_me} {wb_text} {geo_text}"
38
33
  message_dict.update( {'hearing_me':hearing_me, 'wb_text':wb_text, 'geo_text':geo_text, 'display_text':display_text } )
34
+
39
35
  if message_dict['priority']:
40
36
  if self.gui:
41
37
  self.gui.display_message(message_dict)
42
-
43
- self.message_queue_non_time_critical.put(message_dict)
44
-
45
38
  if self.on_decode:
46
39
  self.on_decode(message_dict)
40
+ m = message_dict
41
+ screen_format = f"{cyclestart_string} {their_snr} {m['dt']:4.1f} {m['fHz']:4.0f} ~ {hail} {their_call} {grid_rpt}"
42
+ print(f"{screen_format:50s} decoded@ {m['decode_completed']:5.1f}s, dec = {m['decode_status']}")
43
+ self.message_queue_non_time_critical.put(message_dict)
47
44
 
48
45
  def _process_message_ntc(self):
49
46
  while True:
@@ -11,7 +11,7 @@ from PyFT8.databases import ADIF
11
11
  from PyFT8.qso_manager import QSO_manager
12
12
  from PyFT8.message_broker import Broker
13
13
 
14
- VER = '3.6.2'
14
+ VER = '3.6.4'
15
15
  PSKR_REFRESH_MINS = 20
16
16
  message_broker = None
17
17
 
@@ -129,12 +129,13 @@ def cli():
129
129
  message_broker.soundcard_out = soundcard_out
130
130
  message_broker.adif_logging = ADIF(f"{config_folder}/PyFT8.adi")
131
131
  message_broker.history = History(config_folder, myCall, myGrid, config['gui']['loc'])
132
- configured_bands = []
133
- for b,f in config['bands'].items():
134
- configured_bands.append({'band':b, 'fMHz':f})
132
+ configured_bands = {}
133
+ for b, f in config['bands'].items():
134
+ configured_bands[b] = f
135
135
  message_broker.gui = Gui(message_broker, rig_control, console_print, configured_bands)
136
136
  qso_manager = QSO_manager(message_broker, rig_control, console_print)
137
137
  message_broker.gui.register_qso_manager(qso_manager)
138
+ message_broker.rx.register_before_search(message_broker.gui.before_search)
138
139
  message_broker.rx.register_after_search(message_broker.gui.after_search)
139
140
  message_broker.history.incorporate_log_data(qso_manager.adif_logging.cache)
140
141
  message_broker.history.start_collect_new()
@@ -153,7 +154,7 @@ def cli():
153
154
  pass
154
155
  else:
155
156
  message_broker.gui.set_bandstats_title(f"Pskreporter Spots\nto/from {config['station']['grid'][:4]} <{PSKR_REFRESH_MINS:.0f} mins")
156
- message_broker.gui.monitor_waterfall()
157
+ message_broker.gui.main_loop()
157
158
 
158
159
 
159
160
 
@@ -11,7 +11,7 @@ class QSO_manager:
11
11
  self.find_clear_freq = message_broker.rx.find_clear_freq
12
12
  self.adif_logging = message_broker.adif_logging
13
13
  self.in_qso_with = False
14
- self.tx_payload = None
14
+ self.tx_payload, self.last_tx_payload = None, None
15
15
  self.transmitting = False
16
16
  self.tx_cycle = 0
17
17
  self.console_print = console_print
@@ -481,6 +481,7 @@ class Receiver():
481
481
  self.audio_in = AudioIn(search_freq_range, input_device_keywords, wav_files)
482
482
  self.process_message = message_broker.process_message
483
483
  message_broker.waterfall_data = self.audio_in.waterfall_data
484
+ self.before_search = None
484
485
  self.after_search = None
485
486
  self.sync_score_min, self.max_cands = sync_score_min, max_cands
486
487
  self.wav_files = wav_files
@@ -503,6 +504,9 @@ class Receiver():
503
504
  time_utils.sleep(0.5)
504
505
  threading.Thread(target=self.manage_cycle, daemon=True).start()
505
506
 
507
+ def register_before_search(self, func):
508
+ self.before_search = func
509
+
506
510
  def register_after_search(self, func):
507
511
  self.after_search = func
508
512
 
@@ -518,6 +522,8 @@ class Receiver():
518
522
  return clearest_frequency
519
523
 
520
524
  def search(self, cyclestart, odd_even, cycle_h0):
525
+ if self.before_search is not None:
526
+ self.before_search(odd_even)
521
527
  cands = []
522
528
  hops_per_sig = self.audio_in.search_hps * PAYLOAD_SYMB_IDXS[-1]
523
529
  for f0_idx in range(self.audio_in.search_f0_idx_range[0], self.audio_in.search_f0_idx_range[1], 2):
@@ -583,6 +589,7 @@ class Receiver():
583
589
  duplicate_filter.add(key)
584
590
  hail, their_call, grid_rpt = c.msg_tuple
585
591
  message_dict = {'cyclestart_string':c.cyclestart['string'], 'hail':hail, 'their_call':their_call, 'grid_rpt':grid_rpt,
592
+ 'msg_text':f"{hail} {their_call} {grid_rpt}",
586
593
  'their_snr':f"{c.snr:+03d}", 'their_tx_cycle':c.origin['odd_even'],
587
594
  't0':c.origin['t0'], 'dt':c.origin['t0'] - 0.5, 'fHz':c.origin['f0'],
588
595
  'decode_status':c.decode_status, 'decode_completed':time_utils.cycle_time()}
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: PyFT8
3
- Version: 3.6.2
3
+ Version: 3.6.4
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
@@ -110,11 +110,11 @@ upload = Y
110
110
  2m = 144.174
111
111
  ```
112
112
 
113
- ## Performance Compared with FT8_lib and WSJT-x
113
+ ## Performance Compared with WSJT-x
114
114
 
115
- 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.
115
+ The image below shows the number of decodes from PyFT8, WSJT-x V2.7.0 running in NORM mode, using the same 10 minutes of busy 20m audio that is used to test ft8_lib. On this dataset, WSJT-x performs almost the same on DEEP mode as it does on NORM, and both beat PyFT8. However, PyFT8 gets very close to WSJT-x's performance on FAST mode.
116
116
 
117
- <img width="900" height="898" alt="batch test performance" src="https://github.com/user-attachments/assets/513d4121-5483-4e7b-81df-785dc4db3d37" />
117
+ <img width="900" height="900" alt="live playback vs wsjtx_v2 7 0" src="https://github.com/user-attachments/assets/853e98b2-5b9b-4241-b5f3-181fe193642e" />
118
118
 
119
119
  ## Limitations
120
120
  PyFT8 doesn't decode / encode *all* message types. The table below shows which are handled.
@@ -20,8 +20,10 @@ PyFT8.egg-info/dependency_links.txt
20
20
  PyFT8.egg-info/entry_points.txt
21
21
  PyFT8.egg-info/requires.txt
22
22
  PyFT8.egg-info/top_level.txt
23
- tests/plot_baseline.py
23
+ tests/play_wavs.py
24
+ tests/plot_decode_times.py
24
25
  tests/spare.py
26
+ tests/spare2.py
25
27
  tests/test_batch_and_live.py
26
28
  tests/misc/CQ AAAA.py
27
29
  tests/misc/animation.py
@@ -88,11 +88,11 @@ upload = Y
88
88
  2m = 144.174
89
89
  ```
90
90
 
91
- ## Performance Compared with FT8_lib and WSJT-x
91
+ ## Performance Compared with WSJT-x
92
92
 
93
- 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.
93
+ The image below shows the number of decodes from PyFT8, WSJT-x V2.7.0 running in NORM mode, using the same 10 minutes of busy 20m audio that is used to test ft8_lib. On this dataset, WSJT-x performs almost the same on DEEP mode as it does on NORM, and both beat PyFT8. However, PyFT8 gets very close to WSJT-x's performance on FAST mode.
94
94
 
95
- <img width="900" height="898" alt="batch test performance" src="https://github.com/user-attachments/assets/513d4121-5483-4e7b-81df-785dc4db3d37" />
95
+ <img width="900" height="900" alt="live playback vs wsjtx_v2 7 0" src="https://github.com/user-attachments/assets/853e98b2-5b9b-4241-b5f3-181fe193642e" />
96
96
 
97
97
  ## Limitations
98
98
  PyFT8 doesn't decode / encode *all* message types. The table below shows which are handled.
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "PyFT8"
3
- version = "3.6.2"
3
+ version = "3.6.4"
4
4
  license = "GPL-3.0-or-later"
5
5
 
6
6
  authors = [
@@ -0,0 +1,54 @@
1
+ import threading, pyaudio
2
+ from PyFT8.time_utils import time_utils
3
+ SAMP_RATE =12000
4
+
5
+ class SoundcardOut:
6
+ def __init__(self, outputcard_keywords, wav_files):
7
+ self.output_device_index = None
8
+ self.pya = pyaudio.PyAudio()
9
+ threading.Thread(target = self.play_wavs, args = (wav_files,), daemon = True).start()
10
+
11
+ if outputcard_keywords:
12
+ for dev_idx in range(self.pya.get_device_count()):
13
+ name = self.pya.get_device_info_by_index(dev_idx)['name']
14
+ match = True
15
+ for pattern in outputcard_keywords.replace(' ','').split(','):
16
+ if (not pattern in name): match = False
17
+ if(match):
18
+ self.output_device_index = dev_idx
19
+ break
20
+ if not self.output_device_index:
21
+ time_utils.tlog(f"[Audio Out] No output audio device found matching {outputcard_keywords}", verbose = True)
22
+ sys.exit(1)
23
+
24
+ def play_wavs(self, wav_files, sr=12000):
25
+ import wave
26
+ t = 15.05-time_utils.cycle_time()
27
+ print(t)
28
+ time_utils.sleep(t)
29
+ dt = 0.6/4
30
+ for i, w in enumerate(wav_files):
31
+ print(f"File {i}")
32
+ wv = wave.open(w, 'rb')
33
+ audio_bytes = wv.readframes(sr*16)
34
+ audio_bytes = audio_bytes[-int(sr*(15-dt))*2:]
35
+
36
+ stream = self.pya.open(format=pyaudio.paInt16, channels=1, rate = sr, output=True,
37
+ output_device_index = self.output_device_index)
38
+ stream.write(audio_bytes)
39
+ stream.stop_stream()
40
+ stream.close()
41
+
42
+ def play_wavs(i0, i1):
43
+ wav_files = []
44
+ for idx in range(i0, i1):
45
+ wav_files.append(f"{wav_folder}/test_{idx:02d}.wav")
46
+ soundout = SoundcardOut("CABLE, Input", wav_files)
47
+
48
+ wav_folder = "C:/Users/drala/Documents/Projects/GitHub/ft8_lib/test/wav/20m_busy"
49
+ play_wavs(1,30)
50
+
51
+
52
+
53
+
54
+
@@ -0,0 +1,26 @@
1
+ import numpy as np
2
+ import matplotlib.pyplot as plt
3
+ import pickle
4
+
5
+
6
+ def plot_from_file(ax, fname, label, color):
7
+ print(fname)
8
+ with open(fname, 'rb') as f:
9
+ times = pickle.load(f)
10
+ ax.plot(times, np.array(range(len(times))), label = label, color = color)
11
+
12
+ i0, i1 = 1, 39
13
+
14
+ fig, ax = plt.subplots(figsize = (9,9))
15
+
16
+ plot_from_file(ax, 'live_decode_times_PyFT8.pkl', 'PyFT8', 'limegreen')
17
+
18
+ plot_from_file(ax, 'live_decode_times_WSJTx_2.7.0_1_FAST.pkl', 'WSJT-x_2.7.0_FAST', 'grey')
19
+ plot_from_file(ax, 'live_decode_times_WSJTx_2.7.0_2_NORM.pkl', 'WSJT-x_2.7.0_NORM', 'purple')
20
+ plot_from_file(ax, 'live_decode_times_WSJTx_2.7.0_3_DEEP.pkl', 'WSJT-x_2.7.0_DEEP', 'black')
21
+
22
+ ax.set_xlabel("Time, seconds")
23
+ ax.set_ylabel("Cumulative decodes (unique per cycle)")
24
+ fig.suptitle("Cumulative decode count against time")
25
+ ax.legend()
26
+ plt.show()
@@ -12,11 +12,11 @@ bg = np.random.rand(SIZE,SIZE)
12
12
 
13
13
  def sender():
14
14
  global msg_box_display_queue, bg
15
- x, y = SIZE*np.random.rand(50), SIZE*np.random.rand(50)
16
- for i in range(20):
15
+ while True:
16
+ time.sleep(0.5)
17
17
  bg = np.random.rand(SIZE,SIZE)
18
- time.sleep(0.16)
19
- msg_box_display_queue.put({'x':x[i], 'y':y[i]})
18
+ x, y = SIZE*np.random.rand(1)[0], SIZE*np.random.rand(1)[0]
19
+ msg_box_display_queue.put({'x':x, 'y':y})
20
20
 
21
21
  class Msg_box():
22
22
  def __init__(self, message):
@@ -47,6 +47,6 @@ while True:
47
47
 
48
48
  t = time.time()
49
49
  fig.canvas.draw()
50
- print(time.time() - t)
50
+ #print(time.time() - t)
51
51
  fig.canvas.flush_events()
52
52
  time.sleep(0.01)
@@ -0,0 +1,28 @@
1
+
2
+ import numpy as np
3
+ import matplotlib.pyplot as plt
4
+ import time
5
+ SIZE = 500
6
+
7
+ fig, ax = plt.subplots( figsize=(8,8) )
8
+ ax.set_xlim(0,SIZE)
9
+ ax.set_ylim(0,SIZE)
10
+ plt.show(block = False)
11
+
12
+ arts = []
13
+
14
+ from matplotlib.patches import Rectangle
15
+ for i in range(10):
16
+ x, y = SIZE*np.random.rand(1)[0], SIZE*np.random.rand(1)[0]
17
+ rect = Rectangle((x, y), width=100, height=6, alpha=0.6, edgecolor='lime', lw=2)
18
+ art = ax.add_patch(rect)
19
+ arts.append(art)
20
+ ax.draw_artist(art)
21
+ fig.canvas.update()
22
+ fig.canvas.flush_events()
23
+
24
+ time.sleep(1)
25
+ for a in arts:
26
+ a.remove()
27
+ plt.pause(0.001)
28
+
@@ -5,6 +5,7 @@ from matplotlib.animation import FuncAnimation
5
5
  from PyFT8.time_utils import time_utils
6
6
  from PyFT8.receiver import Receiver
7
7
  from PyFT8.message_broker import Broker
8
+ from PyFT8.transmitter import SoundcardOut
8
9
  from PyFT8.gui import Gui
9
10
 
10
11
  class Wsjtx_all_tailer:
@@ -79,6 +80,7 @@ def test_common(input_source):
79
80
  t_start = time_utils.time()
80
81
 
81
82
  def batch_test(i0, i1):
83
+ soundout = SoundcardOut(["CABLE","Input"])
82
84
  wav_files = []
83
85
  for idx in range(i0, i1):
84
86
  wav_files.append(f"{wav_folder}/test_{idx:02d}.wav")
@@ -109,10 +111,10 @@ def batch_test(i0, i1):
109
111
  plt.show()
110
112
 
111
113
 
112
- def live_test():
114
+ def live_test(audio_source):
113
115
  global both_started
114
116
  both_started = False
115
- test_common(["Mic", "CODEC"])
117
+ test_common(audio_source)
116
118
  wsjtx_all_tailer = Wsjtx_all_tailer(on_wsjtx_decode, silent = True)
117
119
 
118
120
  ws_line = ax.plot([], [], label = 'WSJT-X')[0]
@@ -131,6 +133,10 @@ def live_test():
131
133
  ax.set_ylim(0, n_max)
132
134
  if any(py_times):
133
135
  ax.set_xlim(0, np.max(py_times))
136
+ with open('live_decode_times_PyFT8.pkl', 'wb') as f:
137
+ pickle.dump(py_times, f)
138
+ with open('live_decode_times_WSJTx.pkl', 'wb') as f:
139
+ pickle.dump(ws_times, f)
134
140
  return ws_line, py_line,
135
141
  ani = FuncAnimation(fig, anim, interval = 5000, frames=(100000), blit=False)
136
142
  plt.show()
@@ -142,9 +148,12 @@ data_folder = "C:/Users/drala/Documents/Projects/GitHub/PyFT8/tests/data/ft8_lib
142
148
  wav_folder = "C:/Users/drala/Documents/Projects/GitHub/ft8_lib/test/wav/20m_busy"
143
149
 
144
150
  gui = None
145
-
146
- #live_test()
147
- batch_test(1,39)
151
+ t = 15.05-time_utils.cycle_time()
152
+ print(t)
153
+ time_utils.sleep(t)
154
+ #live_test("Mic, CODEC")
155
+ live_test("CABLE, Output")
156
+ #batch_test(1,39)
148
157
 
149
158
 
150
159
 
pyft8-3.6.2/PyFT8/gui.py DELETED
@@ -1,298 +0,0 @@
1
- import numpy as np
2
- import matplotlib.pyplot as plt
3
- from matplotlib import rcParams
4
- import queue
5
- from matplotlib.widgets import Button
6
- from PyFT8.time_utils import time_utils
7
-
8
- rcParams['toolbar'] = 'None'
9
- MAIN_TEXT_COLOR = '#f0f9fa'
10
- TEXT_BACKGROUND_COLOR = '#2a2b2b'
11
- INFO_TEXT_COLOR = 'white'
12
- BUTTONCOLOR = 'grey'
13
- HOVERCOLOR = 'darkgreen'
14
- ACTIVE_BUTTON_COLOR = 'cyan'
15
- INACTIVE_BUTTON_COLOR = '#edeef0'
16
- MAX_FONT_SIZE_MAIN = 10
17
- L = {'pmargin':0.04, 'sidebar_width': 0.17, 'banner_height':0.1, 'vsep1':0.01, 'hsep1':0.02}
18
-
19
-
20
- class ButtonBox:
21
- def __init__(self, fig, _reset_axis, box, btn_pc = 30, onclick = None, clickargs=None, btn_text = ' ', info_text = ' '):
22
- self._reset_axis = _reset_axis
23
- self.clickargs = clickargs
24
- btnbox, infobox = box.copy(), box.copy()
25
- btnbox[2] = box[2] * btn_pc /100
26
- infobox[2] = box[2] * (100-btn_pc) /100
27
- infobox[0] = box[0] + box[2] * (btn_pc /100)
28
- self.btn_axs = fig.add_axes(btnbox)
29
- self.btn_widg = Button(self.btn_axs, btn_text, color = BUTTONCOLOR, hovercolor = HOVERCOLOR)
30
- self.btn_widg.on_clicked(lambda x: onclick(clickargs))
31
- self.info_axs = fig.add_axes(infobox)
32
- self.info_text_inst = self.info_axs.text(0.03, 0.5, info_text, color = INFO_TEXT_COLOR, verticalalignment = 'center', clip_on = True)
33
- self.set_info_text(info_text)
34
- self.state_is_active = None
35
- self.set_state(False)
36
-
37
- def set_state(self, is_active: bool):
38
- if is_active != self.state_is_active:
39
- self.state_is_active = is_active
40
- color = ACTIVE_BUTTON_COLOR if is_active else INACTIVE_BUTTON_COLOR
41
- self.btn_widg.label.set_color(color)
42
- self.btn_axs.draw_artist(self.btn_widg.label)
43
- if self.info_text_inst:
44
- self.info_text_inst.set_color(color)
45
- self.info_axs.draw_artist(self.info_text_inst)
46
-
47
- def set_info_text(self, info_text):
48
- self._reset_axis(self.info_axs)
49
- self.info_text_inst.set_text(info_text)
50
- self.info_axs.draw_artist(self.info_text_inst)
51
-
52
-
53
- MESSAGE_TYPES = {'generic':{'bg':'blue', 'fg':'white', 'alpha':0.5}, 'CQ':{'bg':'green', 'fg':'white', 'alpha':0.8},
54
- 'from_me': {'bg':'yellow', 'fg':'black', 'alpha':0.95}, 'to_me':{'bg':'red', 'fg':'white', 'alpha':0.9}}
55
- class Msg_box:
56
- def __init__(self, fig, ax, w, h, onclick):
57
- from matplotlib.patches import Rectangle
58
- self.onclick = onclick
59
- self.fig, self.ax = fig, ax
60
- rect = Rectangle((0, 0), w, h, edgecolor='lime', lw=2)
61
- self.patch = self.ax.add_patch(rect)
62
- self.text_inst = self.ax.text(0, 0, '', fontsize='small', fontweight = 'bold' )
63
- self.cid = fig.canvas.mpl_connect('button_press_event', self._onclick)
64
- self.active = False
65
-
66
- def set_properties(self, x, y, message):
67
- self.patch.set_xy((x, y))
68
- self.text_inst.set_position((x, y+1))
69
- self.text_inst.set_text(message['display_text'])
70
- message_type_params = MESSAGE_TYPES[message['message_type']]
71
- self.text_inst.set_color(message_type_params['fg'])
72
- self.patch.set_facecolor(message_type_params['bg'])
73
- self.patch.set_alpha(message_type_params['alpha'])
74
- self.message = message
75
- self.cycle = message['their_tx_cycle']
76
- self.active = True
77
-
78
- def _onclick(self, event):
79
- if self.active:
80
- b, _ = self.patch.contains(event)
81
- if(b):
82
- self.onclick({'action': 'MESSAGE_CLICK', 'message':self.message})
83
-
84
- class Gui:
85
- def __init__(self, message_broker, rig_control, console_print, configured_bands, hearing_me_since_mins = 5):
86
- self.hearing_me_since_mins = hearing_me_since_mins
87
- self.waterfall_data = message_broker.waterfall_data
88
- self.history = message_broker.history
89
- self.qso_manager = None
90
- self.configured_bands = configured_bands
91
- self.console_print = console_print
92
- self.myCall, self.myGrid = message_broker.myCall, message_broker.myGrid
93
- self.band_info = {'current_band': None, 'fMHz':0, 'time_set':0}
94
-
95
- self.plt = plt
96
- self.fig = plt.figure(figsize = (10,10), facecolor=(.18, .71, .71, 0.4))
97
- self.fig.canvas.manager.set_window_title('PyFT8 by G1OJS')
98
- self.wf_top = 1-L['pmargin']-L['banner_height']-L['vsep1']
99
- self.wf_left = L['pmargin']+L['sidebar_width']+L['hsep1']
100
-
101
- self.ax_wf = self.fig.add_axes([self.wf_left, L['pmargin'], 1-self.wf_left-L['pmargin'], self.wf_top-L['pmargin']])
102
- self.ax_wf.set_xticks([])
103
- self.ax_wf.set_yticks([])
104
-
105
- self.image = self.ax_wf.imshow(self.waterfall_data['data'],vmax=120,vmin=90,origin='lower',interpolation='none', aspect = 'auto')
106
- self.ax_ss = self.fig.add_axes([L['pmargin'], self.wf_top+L['vsep1'], L['sidebar_width'], L['banner_height']])
107
- self.ax_cs = self.fig.add_axes([self.wf_left, self.wf_top+L['vsep1'], 1-self.wf_left-L['pmargin'], L['banner_height']])
108
-
109
- self.console_rows = [('','white'),('','white'),('','white'),('','white'),('','white')]
110
- self.hearing_page = 0
111
- self.active_msg_boxes = []
112
- self.inactive_msg_boxes = []
113
- self.button_boxes = []
114
-
115
- bh, bs = 0.02, 0.002
116
- bb = ButtonBox(self.fig, self._reset_axis, [L['pmargin'], self.wf_top - (len(self.button_boxes)+1) * bh + bs, L['sidebar_width'], bh-bs], btn_pc = 100,
117
- btn_text = "CQ", onclick = self._on_click_local, clickargs = {'action':'CQ'})
118
- self.button_boxes.append(bb)
119
- bb = ButtonBox(self.fig, self._reset_axis, [L['pmargin'], self.wf_top - (len(self.button_boxes)+1) * bh + bs, L['sidebar_width'], bh-bs], btn_pc = 100,
120
- btn_text = "Repeat last", onclick = self._on_click_local, clickargs = {'action':'RPT_LAST'})
121
- self.button_boxes.append(bb)
122
- bb = ButtonBox(self.fig, self._reset_axis, [L['pmargin'], self.wf_top - (len(self.button_boxes)+1) * bh + bs, L['sidebar_width'], bh-bs], btn_pc = 100,
123
- btn_text = "Tx off", onclick = self._on_click_local, clickargs = {'action':'TX_OFF'})
124
- self.button_boxes.append(bb)
125
- for band_info in self.configured_bands:
126
- band, fMHz = band_info['band'], band_info['fMHz']
127
- bb = ButtonBox(self.fig, self._reset_axis, [L['pmargin'], self.wf_top - (len(self.button_boxes)+1) * bh + bs, L['sidebar_width'], bh-bs], btn_pc = 30,
128
- btn_text = band, onclick = self._on_click_local, clickargs = {'action':'SET_BAND', 'band':band, 'fMHz':fMHz})
129
- self.button_boxes.append(bb)
130
-
131
- hm_top = self.wf_top - (len(self.button_boxes)+2) * bh + bs - L['vsep1']
132
- self.ax_hm = self.fig.add_axes([L['pmargin'], L['pmargin'], L['sidebar_width'], hm_top])
133
-
134
- self.tlast = 0
135
- self.band_info = {'current_band': None, 'fMHz':0, 'time_set':0}
136
- self._refresh_hearing()
137
- self._refresh_band_buttons()
138
- self._refresh_square_stats()
139
-
140
- def get_band_info(self):
141
- return self.band_info
142
-
143
- def register_qso_manager(self, qsm):
144
- self.qso_manager = qsm
145
-
146
- def set_bandstats_title(self, txt):
147
- self.ax_ss.set_title(txt, fontsize = 10)
148
-
149
- def update_console(self, text, color, nlines = 5):
150
- ax = self.ax_cs
151
- bbox = ax.get_window_extent().transformed(self.fig.dpi_scale_trans.inverted())
152
- rowheight = bbox.height * self.fig.dpi / nlines
153
- fontsize = np.min([0.5 * rowheight, MAX_FONT_SIZE_MAIN])
154
- self._reset_axis(ax)
155
- self.console_rows[1:] = self.console_rows[:-1]
156
- self.console_rows[0] = (text, color)
157
- for i, rw in enumerate(self.console_rows):
158
- ax.draw_artist(self._text_row(ax, 0.03, .03+0.9*i/len(self.console_rows), rw[0], color = rw[1], fontsize = fontsize))
159
-
160
- def monitor_waterfall(self):
161
- last_ptr = 0
162
- self.plt.show(block = False)
163
- t0 = time_utils.time()
164
- while True:
165
- time_utils.sleep(0.05)
166
- self.image.set_data(self.waterfall_data['data'])
167
- self.ax_wf.draw_artist(self.image)
168
- for mb in self.active_msg_boxes:
169
- self.ax_wf.draw_artist(mb.patch)
170
- self.ax_wf.draw_artist(mb.text_inst)
171
- self.fig.canvas.update()
172
- self.fig.canvas.flush_events()
173
- t = time_utils.time()
174
- #print(f"{t - t0:6.3f}")
175
- t0 = t
176
-
177
- def after_search(self, curr_cycle):
178
- self._refresh_hearing()
179
- self._clear_msg_boxes(curr_cycle)
180
- self._refresh_band_buttons()
181
- self._refresh_square_stats()
182
-
183
- def display_message(self, message):
184
- x = int(message['t0'] / self.waterfall_data['dt'] + message['their_tx_cycle'] * self.waterfall_data['pixels_per_cycle'])
185
- y = int(message['fHz'] / self.waterfall_data['df'])
186
- mb = self._get_msg_box()
187
- self.active_msg_boxes.append(mb)
188
- mb.set_properties(x, y, message)
189
-
190
- def _reset_axis(self, ax):
191
- from matplotlib.patches import Rectangle
192
- rect = Rectangle((0, 0), 1, 1, facecolor = TEXT_BACKGROUND_COLOR)
193
- ax.draw_artist(ax.add_patch(rect))
194
- ax.set_xticks([])
195
- ax.set_yticks([])
196
-
197
- def _on_click_local(self, clickargs):
198
- btn_action = clickargs['action']
199
- if(btn_action == 'SET_BAND'):
200
- current_band, freqMHz = clickargs['band'], clickargs['fMHz']
201
- self.band_info = {'current_band':current_band, 'fMHz':freqMHz, 'time_set':time_utils.time()}
202
- self.console_print(f"[PyFT8] Set band: {self.band_info['current_band']} {self.band_info['fMHz']}")
203
- self._refresh_hearing()
204
- for cyc in range(2):
205
- self._clear_msg_boxes(cyc)
206
- self._refresh_band_buttons()
207
- self._refresh_square_stats()
208
- if clickargs['action'] == "MESSAGE_CLICK":
209
- m = clickargs['message']
210
- m_string = f"{m['hail']} {m['their_call']} {m['grid_rpt']}"
211
- self.console_print(f"[GUI] Clicked on message '{m_string}'")
212
- self.qso_manager.on_click(clickargs)
213
-
214
- def _clear_msg_boxes(self, curr_cycle):
215
- to_remove = [mb for mb in self.active_msg_boxes if mb.cycle == curr_cycle]
216
- for mb in to_remove:
217
- mb.active = False
218
- self.inactive_msg_boxes.append(mb)
219
- self.active_msg_boxes.remove(mb)
220
-
221
- def _get_msg_box(self):
222
- if len(self.inactive_msg_boxes) == 0:
223
- mb = Msg_box(self.fig, self.ax_wf, self.waterfall_data['sig_w'], self.waterfall_data['sig_h'], onclick = self._on_click_local)
224
- else:
225
- mb = self.inactive_msg_boxes.pop()
226
- return mb
227
-
228
- def _text_row(self, ax, x, y, text = '', color = 'white', **args):
229
- art = ax.text(x, y, text, color = color)
230
- art.set_fontfamily('monospace')
231
- return art
232
-
233
- def _refresh_square_stats(self):
234
- ax = self.ax_ss
235
- self._reset_axis(ax)
236
- ax.draw_artist(ax.text(-0.2,0.75,'Tx'))
237
- ax.draw_artist(ax.text(-0.2,0.25,'Rx'))
238
- current_band = self.band_info['current_band']
239
- if current_band is not None and self.history is not None:
240
- if current_band in self.history.home_most_remotes:
241
- tx_lead, rx_lead = self.history.home_most_remotes[current_band]
242
- n_spotted, n_spotting = self.history.get_spot_counts(current_band, self.myCall)
243
- ax.draw_artist(self._text_row(ax, 0.03, .75, f"{self.myCall:<7} {tx_lead[0]:<7}", color = '#ff756b' ))
244
- ax.draw_artist(self._text_row(ax, 0.03, .6, f"{n_spotting:<7} {tx_lead[1]:<7}", color = '#ff756b' ))
245
- ax.draw_artist(self._text_row(ax, 0.03, .25, f"{self.myCall:<7} {rx_lead[0]:<7}", color = '#b6f0c6' ))
246
- ax.draw_artist(self._text_row(ax, 0.03, .1, f"{n_spotted:<7} {rx_lead[1]:<7}", color = '#b6f0c6' ))
247
-
248
- def _refresh_band_buttons(self):
249
- current_band = self.band_info['current_band']
250
- grd = self.myGrid[:4]
251
- for bb in self.button_boxes:
252
- button_band = bb.clickargs.get('band',None)
253
- if button_band is not None:
254
- bb.set_state(button_band == current_band)
255
- if button_band in self.history.home_activity:
256
- cnts = self.history.home_activity[button_band]
257
- bb.set_info_text(f"{cnts[0]}Tx, {cnts[1]}Rx")
258
- self.fig.canvas.update()
259
- self.fig.canvas.flush_events()
260
-
261
- def _refresh_hearing(self):
262
- current_band = self.band_info['current_band']
263
- ax = self.ax_hm
264
- self._reset_axis(ax)
265
- line_height = 0.03
266
- bbox = ax.get_window_extent().transformed(self.fig.dpi_scale_trans.inverted())
267
- nlines = int(0.9 / line_height)
268
- row_artists = []
269
- if current_band is not None and self.history is not None:
270
- historic_data = self.history.hearing_me.data if self.hearing_page == 1 else self.history.heard_by_me.data
271
- new_calls_data = self.history.hearing_me_new if self.hearing_page == 1 else self.history.heard_by_me_new
272
- timewindow_str = f"<{self.hearing_me_since_mins:.0f} mins"
273
- title_txt = f"Hearing me {timewindow_str}" if self.hearing_page==1 else f"Heard by me {timewindow_str}"
274
- row_artists.append(self._text_row(ax, 0.03, 1 - line_height, title_txt, color = 'white'))
275
- if current_band in historic_data:
276
- tnow = time_utils.time()
277
- band_rpts = historic_data[current_band]
278
- calls_now = [call for call in band_rpts if (tnow - band_rpts[call]['t']) < 60*self.hearing_me_since_mins]
279
- calls_now.sort(key = lambda c: band_rpts[c]['t'], reverse = True)
280
- subtitle_txt = f"{len(calls_now)}/{len(band_rpts)} now/ever"
281
- row_artists.append(self._text_row(ax, 0.03, 1 - 2*line_height, subtitle_txt, color = 'white'))
282
- for i, remote_call in enumerate(calls_now[:nlines - 3]):
283
- rpt = band_rpts[remote_call]
284
- row_txt = f"{remote_call:<7} {self.history.get_geo_text(remote_call)}"
285
- color = 'white' if self.history.is_in_new_alert(current_band, remote_call, new_calls_data) else 'lime'
286
- row_artists.append(self._text_row(ax, 0.03, 1 - line_height*(i+3.2), row_txt, color = color))
287
- for row_art in row_artists:
288
- ax.draw_artist(row_art)
289
- self.fig.canvas.update()
290
- self.fig.canvas.flush_events()
291
- self.hearing_page = (self.hearing_page +1 )%2
292
-
293
-
294
-
295
-
296
-
297
-
298
-
@@ -1,38 +0,0 @@
1
- import numpy as np
2
- import matplotlib.pyplot as plt
3
- import pickle
4
-
5
- data_folder = "C:/Users/drala/Documents/Projects/GitHub/PyFT8/tests/data/ft8_lib_20m_busy"
6
-
7
- i0, i1 = 1, 39
8
-
9
- def get_cumulative_from_text_files(i0, i1, postfix):
10
- times = []
11
- t = 0
12
- for idx in range(i0, i1):
13
- with open(f"{data_folder}/test_{idx:02d}{postfix}", "r") as f:
14
- t += avg_cycle # approximate until real times folded in
15
- for l in f.readlines():
16
- times.append(t)
17
- return times
18
-
19
- avg_cycle = 15.1
20
-
21
- ws_times = get_cumulative_from_text_files(i0, i1, "_wsjtx_2.7.0_NORM.txt")
22
- fl_times = get_cumulative_from_text_files(i0, i1, "_ft8_lib.txt")
23
-
24
- fig, ax = plt.subplots(figsize = (9,9))
25
- ws_line = ax.plot(ws_times, np.array(range(len(ws_times))), label = 'WSJT-X', color = 'gray')[0]
26
- filename ='baseline.pkl'
27
- with open(filename, 'rb') as f:
28
- py_times = pickle.load(f)
29
- ax.plot(py_times, np.array(range(len(py_times))), label = 'PyFT8 ' + filename, color = 'lightgreen')
30
- ft_line = ax.plot(fl_times, np.array(range(len(fl_times))), label = 'ft8_lib', color = 'blue')[0]
31
-
32
- ax.set_xlabel("Time, seconds")
33
- ax.set_ylabel("Cumulative decodes (unique per cycle)")
34
- fig.suptitle("Cumulative decode count against time")
35
- ax.set_xlim(0, np.max(ws_times) + 15)
36
- ax.set_ylim(0, len(ws_times))
37
- ax.legend()
38
- plt.show()
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes