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.
- {pyft8-3.6.2 → pyft8-3.6.4}/PKG-INFO +4 -4
- pyft8-3.6.4/PyFT8/gui.py +311 -0
- {pyft8-3.6.2 → pyft8-3.6.4}/PyFT8/message_broker.py +5 -8
- {pyft8-3.6.2 → pyft8-3.6.4}/PyFT8/pyft8.py +6 -5
- {pyft8-3.6.2 → pyft8-3.6.4}/PyFT8/qso_manager.py +1 -1
- {pyft8-3.6.2 → pyft8-3.6.4}/PyFT8/receiver.py +7 -0
- {pyft8-3.6.2 → pyft8-3.6.4}/PyFT8.egg-info/PKG-INFO +4 -4
- {pyft8-3.6.2 → pyft8-3.6.4}/PyFT8.egg-info/SOURCES.txt +3 -1
- {pyft8-3.6.2 → pyft8-3.6.4}/README.md +3 -3
- {pyft8-3.6.2 → pyft8-3.6.4}/pyproject.toml +1 -1
- pyft8-3.6.4/tests/play_wavs.py +54 -0
- pyft8-3.6.4/tests/plot_decode_times.py +26 -0
- {pyft8-3.6.2 → pyft8-3.6.4}/tests/spare.py +5 -5
- pyft8-3.6.4/tests/spare2.py +28 -0
- {pyft8-3.6.2 → pyft8-3.6.4}/tests/test_batch_and_live.py +14 -5
- pyft8-3.6.2/PyFT8/gui.py +0 -298
- pyft8-3.6.2/tests/plot_baseline.py +0 -38
- {pyft8-3.6.2 → pyft8-3.6.4}/LICENSE +0 -0
- {pyft8-3.6.2 → pyft8-3.6.4}/MANIFEST.in +0 -0
- {pyft8-3.6.2 → pyft8-3.6.4}/PyFT8/__init__.py +0 -0
- {pyft8-3.6.2 → pyft8-3.6.4}/PyFT8/databases.py +0 -0
- {pyft8-3.6.2 → pyft8-3.6.4}/PyFT8/osd.py +0 -0
- {pyft8-3.6.2 → pyft8-3.6.4}/PyFT8/pskreporter.py +0 -0
- {pyft8-3.6.2 → pyft8-3.6.4}/PyFT8/rigctrl.py +0 -0
- {pyft8-3.6.2 → pyft8-3.6.4}/PyFT8/time_utils.py +0 -0
- {pyft8-3.6.2 → pyft8-3.6.4}/PyFT8/transmitter.py +0 -0
- {pyft8-3.6.2 → pyft8-3.6.4}/PyFT8.egg-info/dependency_links.txt +0 -0
- {pyft8-3.6.2 → pyft8-3.6.4}/PyFT8.egg-info/entry_points.txt +0 -0
- {pyft8-3.6.2 → pyft8-3.6.4}/PyFT8.egg-info/requires.txt +0 -0
- {pyft8-3.6.2 → pyft8-3.6.4}/PyFT8.egg-info/top_level.txt +0 -0
- {pyft8-3.6.2 → pyft8-3.6.4}/setup.cfg +0 -0
- {pyft8-3.6.2 → pyft8-3.6.4}/tests/misc/CQ AAAA.py +0 -0
- {pyft8-3.6.2 → pyft8-3.6.4}/tests/misc/animation.py +0 -0
- {pyft8-3.6.2 → pyft8-3.6.4}/tests/misc/bits_loopback.py +0 -0
- {pyft8-3.6.2 → pyft8-3.6.4}/tests/misc/bits_loopback_short.py +0 -0
- {pyft8-3.6.2 → pyft8-3.6.4}/tests/misc/osd.py +0 -0
- {pyft8-3.6.2 → pyft8-3.6.4}/tests/misc/symbols_loopback.py +0 -0
- {pyft8-3.6.2 → pyft8-3.6.4}/tests/misc/test_generate_wav.py +0 -0
- {pyft8-3.6.2 → pyft8-3.6.4}/tests/misc/test_loopback_performance.py +0 -0
- {pyft8-3.6.2 → pyft8-3.6.4}/tests/misc/text.py +0 -0
- {pyft8-3.6.2 → pyft8-3.6.4}/tests/misc/view_worked_before.py +0 -0
- {pyft8-3.6.2 → pyft8-3.6.4}/tests/pipeline/Fine tuning compare IOS_PC.py +0 -0
- {pyft8-3.6.2 → pyft8-3.6.4}/tests/pipeline/Targetted tests.py +0 -0
- {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.
|
|
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
|
|
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.
|
pyft8-3.6.4/PyFT8/gui.py
ADDED
|
@@ -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.
|
|
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
|
|
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.
|
|
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.
|
|
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
|
|
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
|
|
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
|
|
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
|