PyFT8 3.6.2__tar.gz → 3.6.3__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 (43) hide show
  1. {pyft8-3.6.2 → pyft8-3.6.3}/PKG-INFO +4 -4
  2. {pyft8-3.6.2 → pyft8-3.6.3}/PyFT8/gui.py +169 -146
  3. {pyft8-3.6.2 → pyft8-3.6.3}/PyFT8/pyft8.py +3 -2
  4. {pyft8-3.6.2 → pyft8-3.6.3}/PyFT8/receiver.py +6 -0
  5. {pyft8-3.6.2 → pyft8-3.6.3}/PyFT8.egg-info/PKG-INFO +4 -4
  6. {pyft8-3.6.2 → pyft8-3.6.3}/PyFT8.egg-info/SOURCES.txt +3 -1
  7. {pyft8-3.6.2 → pyft8-3.6.3}/README.md +3 -3
  8. {pyft8-3.6.2 → pyft8-3.6.3}/pyproject.toml +1 -1
  9. pyft8-3.6.3/tests/play_wavs.py +54 -0
  10. pyft8-3.6.3/tests/plot_decode_times.py +26 -0
  11. {pyft8-3.6.2 → pyft8-3.6.3}/tests/spare.py +5 -5
  12. pyft8-3.6.3/tests/spare2.py +28 -0
  13. {pyft8-3.6.2 → pyft8-3.6.3}/tests/test_batch_and_live.py +14 -5
  14. pyft8-3.6.2/tests/plot_baseline.py +0 -38
  15. {pyft8-3.6.2 → pyft8-3.6.3}/LICENSE +0 -0
  16. {pyft8-3.6.2 → pyft8-3.6.3}/MANIFEST.in +0 -0
  17. {pyft8-3.6.2 → pyft8-3.6.3}/PyFT8/__init__.py +0 -0
  18. {pyft8-3.6.2 → pyft8-3.6.3}/PyFT8/databases.py +0 -0
  19. {pyft8-3.6.2 → pyft8-3.6.3}/PyFT8/message_broker.py +0 -0
  20. {pyft8-3.6.2 → pyft8-3.6.3}/PyFT8/osd.py +0 -0
  21. {pyft8-3.6.2 → pyft8-3.6.3}/PyFT8/pskreporter.py +0 -0
  22. {pyft8-3.6.2 → pyft8-3.6.3}/PyFT8/qso_manager.py +0 -0
  23. {pyft8-3.6.2 → pyft8-3.6.3}/PyFT8/rigctrl.py +0 -0
  24. {pyft8-3.6.2 → pyft8-3.6.3}/PyFT8/time_utils.py +0 -0
  25. {pyft8-3.6.2 → pyft8-3.6.3}/PyFT8/transmitter.py +0 -0
  26. {pyft8-3.6.2 → pyft8-3.6.3}/PyFT8.egg-info/dependency_links.txt +0 -0
  27. {pyft8-3.6.2 → pyft8-3.6.3}/PyFT8.egg-info/entry_points.txt +0 -0
  28. {pyft8-3.6.2 → pyft8-3.6.3}/PyFT8.egg-info/requires.txt +0 -0
  29. {pyft8-3.6.2 → pyft8-3.6.3}/PyFT8.egg-info/top_level.txt +0 -0
  30. {pyft8-3.6.2 → pyft8-3.6.3}/setup.cfg +0 -0
  31. {pyft8-3.6.2 → pyft8-3.6.3}/tests/misc/CQ AAAA.py +0 -0
  32. {pyft8-3.6.2 → pyft8-3.6.3}/tests/misc/animation.py +0 -0
  33. {pyft8-3.6.2 → pyft8-3.6.3}/tests/misc/bits_loopback.py +0 -0
  34. {pyft8-3.6.2 → pyft8-3.6.3}/tests/misc/bits_loopback_short.py +0 -0
  35. {pyft8-3.6.2 → pyft8-3.6.3}/tests/misc/osd.py +0 -0
  36. {pyft8-3.6.2 → pyft8-3.6.3}/tests/misc/symbols_loopback.py +0 -0
  37. {pyft8-3.6.2 → pyft8-3.6.3}/tests/misc/test_generate_wav.py +0 -0
  38. {pyft8-3.6.2 → pyft8-3.6.3}/tests/misc/test_loopback_performance.py +0 -0
  39. {pyft8-3.6.2 → pyft8-3.6.3}/tests/misc/text.py +0 -0
  40. {pyft8-3.6.2 → pyft8-3.6.3}/tests/misc/view_worked_before.py +0 -0
  41. {pyft8-3.6.2 → pyft8-3.6.3}/tests/pipeline/Fine tuning compare IOS_PC.py +0 -0
  42. {pyft8-3.6.2 → pyft8-3.6.3}/tests/pipeline/Targetted tests.py +0 -0
  43. {pyft8-3.6.2 → pyft8-3.6.3}/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.3
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.
@@ -16,21 +16,95 @@ INACTIVE_BUTTON_COLOR = '#edeef0'
16
16
  MAX_FONT_SIZE_MAIN = 10
17
17
  L = {'pmargin':0.04, 'sidebar_width': 0.17, 'banner_height':0.1, 'vsep1':0.01, 'hsep1':0.02}
18
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, onclick):
30
+ from matplotlib.patches import Rectangle
31
+ self.onclick = onclick
32
+ self.fig, self.ax = fig, ax
33
+ message_type_params = MESSAGE_TYPES[message['message_type']]
34
+ rect = Rectangle((x, y), w, h, edgecolor='lime', lw=2)
35
+ self.patch = self.ax.add_patch(rect)
36
+ self.patch.set_visible(False)
37
+ text_inst = self.ax.text(x, y+1, message['display_text'], fontsize='small', fontweight = 'bold' )
38
+ text_inst.set_visible(False)
39
+ self.cid = fig.canvas.mpl_connect('button_press_event', self._onclick)
40
+ text_inst.set_color(message_type_params['fg'])
41
+ self.patch.set_facecolor(message_type_params['bg'])
42
+ self.patch.set_alpha(message_type_params['alpha'])
43
+ self.message = message
44
+ self.cycle = message['their_tx_cycle']
45
+ self.artists = [self.patch, text_inst]
46
+ for a in self.artists:
47
+ a.set_visible(True)
48
+
49
+ def draw(self):
50
+ for a in self.artists:
51
+ self.ax.draw_artist(a)
52
+
53
+ def hide(self):
54
+ for a in self.artists:
55
+ a.set_visible(False)
56
+
57
+ def remove(self):
58
+ for a in self.artists:
59
+ a.set_visible(False)
60
+ a.remove()
61
+ self.artists.remove(a)
62
+ self = None
63
+
64
+ def _onclick(self, event):
65
+ b, _ = self.patch.contains(event)
66
+ if(b):
67
+ self.onclick({'action': 'MESSAGE_CLICK', 'message':self.message})
68
+
69
+ class Panel:
70
+ def __init__(self, fig, pos):
71
+ self.fig = fig
72
+ self.ax = self.fig.add_axes(pos)
73
+ self.ax.set_xticks([])
74
+ self.ax.set_yticks([])
75
+ self.ax.set_facecolor(TEXT_BACKGROUND_COLOR)
76
+ box_h = self.ax.get_window_extent().height
77
+ font_h = 10
78
+ self.nlines = int(0.5*box_h / font_h)
79
+ self.artists = []
80
+
81
+ def clear(self):
82
+ for a in self.artists:
83
+ a.set_visible(False)
84
+ a.remove()
85
+ self.artists = [a for a in self.artists if a.get_visible()]
86
+
87
+ def print_row(self, row_text, row_number, color = None):
88
+ a = self.ax.text(0.03, 0.03+(self.nlines - row_number)/self.nlines, row_text)
89
+ a.set_fontfamily('monospace')
90
+ if color is not None:
91
+ a.set_color(color)
92
+ self.artists.append(a)
93
+ self.ax.draw_artist(a)
19
94
 
20
95
  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
96
+ def __init__(self, fig, box, btn_pc = 30, onclick = None, clickargs=None, btn_text = ' ', info_text = ' '):
23
97
  self.clickargs = clickargs
24
98
  btnbox, infobox = box.copy(), box.copy()
25
99
  btnbox[2] = box[2] * btn_pc /100
26
100
  infobox[2] = box[2] * (100-btn_pc) /100
27
101
  infobox[0] = box[0] + box[2] * (btn_pc /100)
28
- self.btn_axs = fig.add_axes(btnbox)
102
+ self.btn_axs = add_my_axes(fig, btnbox)
103
+ self.btn_axs.set_facecolor(BUTTONCOLOR)
29
104
  self.btn_widg = Button(self.btn_axs, btn_text, color = BUTTONCOLOR, hovercolor = HOVERCOLOR)
30
105
  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)
106
+ self.info_axs = add_my_axes(fig, infobox)
107
+ self.info_art = self.info_axs.text(0.03, 0.5, info_text, color = INFO_TEXT_COLOR, verticalalignment = 'center', clip_on = True)
34
108
  self.state_is_active = None
35
109
  self.set_state(False)
36
110
 
@@ -39,48 +113,14 @@ class ButtonBox:
39
113
  self.state_is_active = is_active
40
114
  color = ACTIVE_BUTTON_COLOR if is_active else INACTIVE_BUTTON_COLOR
41
115
  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)
116
+ self.set_info_text(info_text = None, color = color)
51
117
 
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
-
118
+ def set_info_text(self, info_text, color = None):
119
+ if info_text is not None:
120
+ self.info_art.set_text(info_text)
121
+ if color is not None:
122
+ self.info_art.set_color(color)
123
+
84
124
  class Gui:
85
125
  def __init__(self, message_broker, rig_control, console_print, configured_bands, hearing_me_since_mins = 5):
86
126
  self.hearing_me_since_mins = hearing_me_since_mins
@@ -97,102 +137,97 @@ class Gui:
97
137
  self.fig.canvas.manager.set_window_title('PyFT8 by G1OJS')
98
138
  self.wf_top = 1-L['pmargin']-L['banner_height']-L['vsep1']
99
139
  self.wf_left = L['pmargin']+L['sidebar_width']+L['hsep1']
140
+ self.needs_redraw = False
100
141
 
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
-
142
+ self.ax_wf = add_my_axes(self.fig, [self.wf_left, L['pmargin'], 1-self.wf_left-L['pmargin'], self.wf_top-L['pmargin']])
105
143
  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
144
 
109
- self.console_rows = [('','white'),('','white'),('','white'),('','white'),('','white')]
145
+ self.home_panel = Panel(self.fig, [L['pmargin'], self.wf_top+L['vsep1'], L['sidebar_width'], L['banner_height']])
146
+ ax = self.home_panel.ax
147
+ ax.draw_artist(ax.text(-0.15,0.75,'Tx'))
148
+ ax.draw_artist(ax.text(-0.15,0.125,'Rx'))
149
+
110
150
  self.hearing_page = 0
111
- self.active_msg_boxes = []
112
- self.inactive_msg_boxes = []
151
+ self.msg_boxes = []
113
152
  self.button_boxes = []
114
153
 
115
154
  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,
155
+ bb = ButtonBox(self.fig, [L['pmargin'], self.wf_top - (len(self.button_boxes)+1) * bh + bs, L['sidebar_width'], bh-bs], btn_pc = 100,
117
156
  btn_text = "CQ", onclick = self._on_click_local, clickargs = {'action':'CQ'})
118
157
  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,
158
+ bb = ButtonBox(self.fig, [L['pmargin'], self.wf_top - (len(self.button_boxes)+1) * bh + bs, L['sidebar_width'], bh-bs], btn_pc = 100,
120
159
  btn_text = "Repeat last", onclick = self._on_click_local, clickargs = {'action':'RPT_LAST'})
121
160
  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,
161
+ bb = ButtonBox(self.fig, [L['pmargin'], self.wf_top - (len(self.button_boxes)+1) * bh + bs, L['sidebar_width'], bh-bs], btn_pc = 100,
123
162
  btn_text = "Tx off", onclick = self._on_click_local, clickargs = {'action':'TX_OFF'})
124
163
  self.button_boxes.append(bb)
125
164
  for band_info in self.configured_bands:
126
165
  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,
166
+ bb = ButtonBox(self.fig, [L['pmargin'], self.wf_top - (len(self.button_boxes)+1) * bh + bs, L['sidebar_width'], bh-bs], btn_pc = 30,
128
167
  btn_text = band, onclick = self._on_click_local, clickargs = {'action':'SET_BAND', 'band':band, 'fMHz':fMHz})
129
168
  self.button_boxes.append(bb)
130
169
 
131
170
  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
171
 
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()
172
+ self.console = Panel(self.fig, [self.wf_left, self.wf_top+L['vsep1'], 1-self.wf_left-L['pmargin'], L['banner_height']])
173
+ self.console_rows_text = None
174
+ self.hearing_panel = Panel(self.fig, [L['pmargin'], L['pmargin'], L['sidebar_width'], hm_top])
139
175
 
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):
176
+ def main_loop(self):
161
177
  last_ptr = 0
162
178
  self.plt.show(block = False)
163
179
  t0 = time_utils.time()
164
180
  while True:
181
+ t = time_utils.time()
182
+ #print(f"{t-t0:6.3f}")
183
+ t0=t
165
184
  time_utils.sleep(0.05)
166
185
  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):
186
+ if self.needs_redraw:
187
+ self.needs_redraw = False
188
+ plt.pause(0.1)
189
+ else:
190
+ self.ax_wf.draw_artist(self.image)
191
+ self.fig.canvas.update()
192
+ for mb in self.msg_boxes:
193
+ mb.draw()
194
+ self.fig.canvas.update()
195
+ self.fig.canvas.flush_events()
196
+
197
+ def before_search(self, curr_cycle):
198
+ self._hide_msg_boxes(curr_cycle)
199
+
200
+ def after_search(self,curr_cycle):
178
201
  self._refresh_hearing()
179
- self._clear_msg_boxes(curr_cycle)
180
202
  self._refresh_band_buttons()
181
- self._refresh_square_stats()
203
+ self._refresh_home_panel()
204
+ self._clear_msg_boxes(curr_cycle)
205
+ self.needs_redraw = True
182
206
 
183
207
  def display_message(self, message):
184
208
  x = int(message['t0'] / self.waterfall_data['dt'] + message['their_tx_cycle'] * self.waterfall_data['pixels_per_cycle'])
185
209
  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)
210
+ mb = Msg_box(self.fig, self.ax_wf, x, y, self.waterfall_data['sig_w'], self.waterfall_data['sig_h'], message, onclick = self._on_click_local)
211
+ self.msg_boxes.append(mb)
212
+ def get_band_info(self):
213
+ return self.band_info
189
214
 
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([])
215
+ def register_qso_manager(self, qsm):
216
+ self.qso_manager = qsm
217
+
218
+ def set_bandstats_title(self, txt):
219
+ self.home_panel.ax.set_title(txt, fontsize = 10)
220
+
221
+ def update_console(self, text, color):
222
+ if self.console_rows_text is None:
223
+ self.console_rows_text = [{'text':'','color':'white'} for i in range(self.console.nlines)]
224
+ self.console_rows_text[1:] = self.console_rows_text[:-1]
225
+ self.console_rows_text[0] = {'text':text,'color':color}
226
+ self.console.clear()
227
+ for row_number, row_text in enumerate(self.console_rows_text):
228
+ self.console.print_row(row_text['text'], self.console.nlines - row_number,
229
+ color = row_text['color'])
230
+ self.needs_redraw = True
196
231
 
197
232
  def _on_click_local(self, clickargs):
198
233
  btn_action = clickargs['action']
@@ -204,46 +239,44 @@ class Gui:
204
239
  for cyc in range(2):
205
240
  self._clear_msg_boxes(cyc)
206
241
  self._refresh_band_buttons()
207
- self._refresh_square_stats()
242
+ self._refresh_home_panel()
208
243
  if clickargs['action'] == "MESSAGE_CLICK":
209
244
  m = clickargs['message']
210
245
  m_string = f"{m['hail']} {m['their_call']} {m['grid_rpt']}"
211
246
  self.console_print(f"[GUI] Clicked on message '{m_string}'")
247
+ self.needs_redraw = True
212
248
  self.qso_manager.on_click(clickargs)
213
249
 
250
+
251
+ def _hide_msg_boxes(self, curr_cycle):
252
+ to_hide = [mb for mb in self.msg_boxes if mb.cycle == curr_cycle]
253
+ for mb in to_hide:
254
+ mb.hide()
255
+ self.needs_redraw = True
256
+
214
257
  def _clear_msg_boxes(self, curr_cycle):
215
- to_remove = [mb for mb in self.active_msg_boxes if mb.cycle == curr_cycle]
258
+ to_remove = [mb for mb in self.msg_boxes if mb.cycle == curr_cycle]
259
+ self.msg_boxes = [mb for mb in self.msg_boxes if mb.cycle != curr_cycle]
216
260
  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
261
+ mb.remove()
262
+ self.needs_redraw = True
227
263
 
228
264
  def _text_row(self, ax, x, y, text = '', color = 'white', **args):
229
265
  art = ax.text(x, y, text, color = color)
230
266
  art.set_fontfamily('monospace')
231
267
  return art
232
268
 
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'))
269
+ def _refresh_home_panel(self):
238
270
  current_band = self.band_info['current_band']
239
271
  if current_band is not None and self.history is not None:
240
272
  if current_band in self.history.home_most_remotes:
241
273
  tx_lead, rx_lead = self.history.home_most_remotes[current_band]
242
274
  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' ))
275
+ self.home_panel.clear()
276
+ self.home_panel.print_row(f"{self.myCall:<7} {tx_lead[0]:<7}", 1, color = '#ff756b' )
277
+ self.home_panel.print_row(f"{n_spotting:<7} {tx_lead[1]:<7}", 2, color = '#ff756b' )
278
+ self.home_panel.print_row(f"{self.myCall:<7} {rx_lead[0]:<7}", 4, color = '#b6f0c6' )
279
+ self.home_panel.print_row(f"{n_spotted:<7} {rx_lead[1]:<7}", 5, color = '#b6f0c6' )
247
280
 
248
281
  def _refresh_band_buttons(self):
249
282
  current_band = self.band_info['current_band']
@@ -255,39 +288,29 @@ class Gui:
255
288
  if button_band in self.history.home_activity:
256
289
  cnts = self.history.home_activity[button_band]
257
290
  bb.set_info_text(f"{cnts[0]}Tx, {cnts[1]}Rx")
258
- self.fig.canvas.update()
259
- self.fig.canvas.flush_events()
291
+ self.needs_redraw = True
260
292
 
261
293
  def _refresh_hearing(self):
262
294
  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
295
  if current_band is not None and self.history is not None:
270
296
  historic_data = self.history.hearing_me.data if self.hearing_page == 1 else self.history.heard_by_me.data
271
297
  new_calls_data = self.history.hearing_me_new if self.hearing_page == 1 else self.history.heard_by_me_new
272
298
  timewindow_str = f"<{self.hearing_me_since_mins:.0f} mins"
273
299
  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'))
300
+ self.hearing_panel.clear()
301
+ self.hearing_panel.print_row(title_txt, 2, color = 'white')
275
302
  if current_band in historic_data:
276
303
  tnow = time_utils.time()
277
304
  band_rpts = historic_data[current_band]
278
305
  calls_now = [call for call in band_rpts if (tnow - band_rpts[call]['t']) < 60*self.hearing_me_since_mins]
279
306
  calls_now.sort(key = lambda c: band_rpts[c]['t'], reverse = True)
280
307
  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]):
308
+ self.hearing_panel.print_row(subtitle_txt, 3, color = 'white')
309
+ for i, remote_call in enumerate(calls_now[:self.hearing_panel.nlines - 3]):
283
310
  rpt = band_rpts[remote_call]
284
311
  row_txt = f"{remote_call:<7} {self.history.get_geo_text(remote_call)}"
285
312
  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()
313
+ self.hearing_panel.print_row(row_txt, i+4, color = color)
291
314
  self.hearing_page = (self.hearing_page +1 )%2
292
315
 
293
316
 
@@ -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.3'
15
15
  PSKR_REFRESH_MINS = 20
16
16
  message_broker = None
17
17
 
@@ -135,6 +135,7 @@ def cli():
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
 
@@ -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):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: PyFT8
3
- Version: 3.6.2
3
+ Version: 3.6.3
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.3"
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
 
@@ -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
File without changes
File without changes