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.
- {pyft8-3.6.2 → pyft8-3.6.3}/PKG-INFO +4 -4
- {pyft8-3.6.2 → pyft8-3.6.3}/PyFT8/gui.py +169 -146
- {pyft8-3.6.2 → pyft8-3.6.3}/PyFT8/pyft8.py +3 -2
- {pyft8-3.6.2 → pyft8-3.6.3}/PyFT8/receiver.py +6 -0
- {pyft8-3.6.2 → pyft8-3.6.3}/PyFT8.egg-info/PKG-INFO +4 -4
- {pyft8-3.6.2 → pyft8-3.6.3}/PyFT8.egg-info/SOURCES.txt +3 -1
- {pyft8-3.6.2 → pyft8-3.6.3}/README.md +3 -3
- {pyft8-3.6.2 → pyft8-3.6.3}/pyproject.toml +1 -1
- pyft8-3.6.3/tests/play_wavs.py +54 -0
- pyft8-3.6.3/tests/plot_decode_times.py +26 -0
- {pyft8-3.6.2 → pyft8-3.6.3}/tests/spare.py +5 -5
- pyft8-3.6.3/tests/spare2.py +28 -0
- {pyft8-3.6.2 → pyft8-3.6.3}/tests/test_batch_and_live.py +14 -5
- pyft8-3.6.2/tests/plot_baseline.py +0 -38
- {pyft8-3.6.2 → pyft8-3.6.3}/LICENSE +0 -0
- {pyft8-3.6.2 → pyft8-3.6.3}/MANIFEST.in +0 -0
- {pyft8-3.6.2 → pyft8-3.6.3}/PyFT8/__init__.py +0 -0
- {pyft8-3.6.2 → pyft8-3.6.3}/PyFT8/databases.py +0 -0
- {pyft8-3.6.2 → pyft8-3.6.3}/PyFT8/message_broker.py +0 -0
- {pyft8-3.6.2 → pyft8-3.6.3}/PyFT8/osd.py +0 -0
- {pyft8-3.6.2 → pyft8-3.6.3}/PyFT8/pskreporter.py +0 -0
- {pyft8-3.6.2 → pyft8-3.6.3}/PyFT8/qso_manager.py +0 -0
- {pyft8-3.6.2 → pyft8-3.6.3}/PyFT8/rigctrl.py +0 -0
- {pyft8-3.6.2 → pyft8-3.6.3}/PyFT8/time_utils.py +0 -0
- {pyft8-3.6.2 → pyft8-3.6.3}/PyFT8/transmitter.py +0 -0
- {pyft8-3.6.2 → pyft8-3.6.3}/PyFT8.egg-info/dependency_links.txt +0 -0
- {pyft8-3.6.2 → pyft8-3.6.3}/PyFT8.egg-info/entry_points.txt +0 -0
- {pyft8-3.6.2 → pyft8-3.6.3}/PyFT8.egg-info/requires.txt +0 -0
- {pyft8-3.6.2 → pyft8-3.6.3}/PyFT8.egg-info/top_level.txt +0 -0
- {pyft8-3.6.2 → pyft8-3.6.3}/setup.cfg +0 -0
- {pyft8-3.6.2 → pyft8-3.6.3}/tests/misc/CQ AAAA.py +0 -0
- {pyft8-3.6.2 → pyft8-3.6.3}/tests/misc/animation.py +0 -0
- {pyft8-3.6.2 → pyft8-3.6.3}/tests/misc/bits_loopback.py +0 -0
- {pyft8-3.6.2 → pyft8-3.6.3}/tests/misc/bits_loopback_short.py +0 -0
- {pyft8-3.6.2 → pyft8-3.6.3}/tests/misc/osd.py +0 -0
- {pyft8-3.6.2 → pyft8-3.6.3}/tests/misc/symbols_loopback.py +0 -0
- {pyft8-3.6.2 → pyft8-3.6.3}/tests/misc/test_generate_wav.py +0 -0
- {pyft8-3.6.2 → pyft8-3.6.3}/tests/misc/test_loopback_performance.py +0 -0
- {pyft8-3.6.2 → pyft8-3.6.3}/tests/misc/text.py +0 -0
- {pyft8-3.6.2 → pyft8-3.6.3}/tests/misc/view_worked_before.py +0 -0
- {pyft8-3.6.2 → pyft8-3.6.3}/tests/pipeline/Fine tuning compare IOS_PC.py +0 -0
- {pyft8-3.6.2 → pyft8-3.6.3}/tests/pipeline/Targetted tests.py +0 -0
- {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.
|
|
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
|
|
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,
|
|
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="
|
|
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,
|
|
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
|
|
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
|
|
32
|
-
self.
|
|
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.
|
|
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
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
|
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.
|
|
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.
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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.
|
|
135
|
-
self.
|
|
136
|
-
self.
|
|
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
|
|
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.
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
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.
|
|
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.
|
|
187
|
-
self.
|
|
188
|
-
|
|
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
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
ax.
|
|
195
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
218
|
-
|
|
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
|
|
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
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
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,
|
|
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="
|
|
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/
|
|
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
|
|
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,
|
|
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="
|
|
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.
|
|
@@ -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
|
-
|
|
16
|
-
|
|
15
|
+
while True:
|
|
16
|
+
time.sleep(0.5)
|
|
17
17
|
bg = np.random.rand(SIZE,SIZE)
|
|
18
|
-
|
|
19
|
-
msg_box_display_queue.put({'x':x
|
|
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(
|
|
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
|
-
|
|
147
|
-
|
|
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
|
|
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
|