PyFT8 2.1.3__tar.gz → 2.1.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-2.1.3 → pyft8-2.1.4}/PKG-INFO +7 -4
- {pyft8-2.1.3 → pyft8-2.1.4}/PyFT8/gui.py +53 -30
- {pyft8-2.1.3 → pyft8-2.1.4}/PyFT8/pyft8.py +34 -23
- {pyft8-2.1.3 → pyft8-2.1.4}/PyFT8/receiver.py +9 -1
- {pyft8-2.1.3 → pyft8-2.1.4}/PyFT8.egg-info/PKG-INFO +7 -4
- pyft8-2.1.4/PyFT8.egg-info/SOURCES.txt +23 -0
- {pyft8-2.1.3 → pyft8-2.1.4}/README.md +6 -3
- {pyft8-2.1.3 → pyft8-2.1.4}/pyproject.toml +1 -1
- {pyft8-2.1.3 → pyft8-2.1.4}/tests/test_batch_and_live.py +11 -11
- pyft8-2.1.3/PyFT8.egg-info/SOURCES.txt +0 -61
- pyft8-2.1.3/tests/data/ft8_lib_20m_busy/test_01.wav +0 -0
- pyft8-2.1.3/tests/data/ft8_lib_20m_busy/test_02.wav +0 -0
- pyft8-2.1.3/tests/data/ft8_lib_20m_busy/test_03.wav +0 -0
- pyft8-2.1.3/tests/data/ft8_lib_20m_busy/test_04.wav +0 -0
- pyft8-2.1.3/tests/data/ft8_lib_20m_busy/test_05.wav +0 -0
- pyft8-2.1.3/tests/data/ft8_lib_20m_busy/test_06.wav +0 -0
- pyft8-2.1.3/tests/data/ft8_lib_20m_busy/test_07.wav +0 -0
- pyft8-2.1.3/tests/data/ft8_lib_20m_busy/test_08.wav +0 -0
- pyft8-2.1.3/tests/data/ft8_lib_20m_busy/test_09.wav +0 -0
- pyft8-2.1.3/tests/data/ft8_lib_20m_busy/test_10.wav +0 -0
- pyft8-2.1.3/tests/data/ft8_lib_20m_busy/test_11.wav +0 -0
- pyft8-2.1.3/tests/data/ft8_lib_20m_busy/test_12.wav +0 -0
- pyft8-2.1.3/tests/data/ft8_lib_20m_busy/test_13.wav +0 -0
- pyft8-2.1.3/tests/data/ft8_lib_20m_busy/test_14.wav +0 -0
- pyft8-2.1.3/tests/data/ft8_lib_20m_busy/test_15.wav +0 -0
- pyft8-2.1.3/tests/data/ft8_lib_20m_busy/test_16.wav +0 -0
- pyft8-2.1.3/tests/data/ft8_lib_20m_busy/test_17.wav +0 -0
- pyft8-2.1.3/tests/data/ft8_lib_20m_busy/test_18.wav +0 -0
- pyft8-2.1.3/tests/data/ft8_lib_20m_busy/test_19.wav +0 -0
- pyft8-2.1.3/tests/data/ft8_lib_20m_busy/test_20.wav +0 -0
- pyft8-2.1.3/tests/data/ft8_lib_20m_busy/test_21.wav +0 -0
- pyft8-2.1.3/tests/data/ft8_lib_20m_busy/test_22.wav +0 -0
- pyft8-2.1.3/tests/data/ft8_lib_20m_busy/test_23.wav +0 -0
- pyft8-2.1.3/tests/data/ft8_lib_20m_busy/test_24.wav +0 -0
- pyft8-2.1.3/tests/data/ft8_lib_20m_busy/test_25.wav +0 -0
- pyft8-2.1.3/tests/data/ft8_lib_20m_busy/test_26.wav +0 -0
- pyft8-2.1.3/tests/data/ft8_lib_20m_busy/test_27.wav +0 -0
- pyft8-2.1.3/tests/data/ft8_lib_20m_busy/test_28.wav +0 -0
- pyft8-2.1.3/tests/data/ft8_lib_20m_busy/test_29.wav +0 -0
- pyft8-2.1.3/tests/data/ft8_lib_20m_busy/test_30.wav +0 -0
- pyft8-2.1.3/tests/data/ft8_lib_20m_busy/test_31.wav +0 -0
- pyft8-2.1.3/tests/data/ft8_lib_20m_busy/test_32.wav +0 -0
- pyft8-2.1.3/tests/data/ft8_lib_20m_busy/test_33.wav +0 -0
- pyft8-2.1.3/tests/data/ft8_lib_20m_busy/test_34.wav +0 -0
- pyft8-2.1.3/tests/data/ft8_lib_20m_busy/test_35.wav +0 -0
- pyft8-2.1.3/tests/data/ft8_lib_20m_busy/test_36.wav +0 -0
- pyft8-2.1.3/tests/data/ft8_lib_20m_busy/test_37.wav +0 -0
- pyft8-2.1.3/tests/data/ft8_lib_20m_busy/test_38.wav +0 -0
- {pyft8-2.1.3 → pyft8-2.1.4}/LICENSE +0 -0
- {pyft8-2.1.3 → pyft8-2.1.4}/MANIFEST.in +0 -0
- {pyft8-2.1.3 → pyft8-2.1.4}/PyFT8/__init__.py +0 -0
- {pyft8-2.1.3 → pyft8-2.1.4}/PyFT8/rigctrl.py +0 -0
- {pyft8-2.1.3 → pyft8-2.1.4}/PyFT8/time_utils.py +0 -0
- {pyft8-2.1.3 → pyft8-2.1.4}/PyFT8/transmitter.py +0 -0
- {pyft8-2.1.3 → pyft8-2.1.4}/PyFT8.egg-info/dependency_links.txt +0 -0
- {pyft8-2.1.3 → pyft8-2.1.4}/PyFT8.egg-info/entry_points.txt +0 -0
- {pyft8-2.1.3 → pyft8-2.1.4}/PyFT8.egg-info/requires.txt +0 -0
- {pyft8-2.1.3 → pyft8-2.1.4}/PyFT8.egg-info/top_level.txt +0 -0
- {pyft8-2.1.3 → pyft8-2.1.4}/setup.cfg +0 -0
- {pyft8-2.1.3 → pyft8-2.1.4}/tests/dev/osd.py +0 -0
- {pyft8-2.1.3 → pyft8-2.1.4}/tests/dev/test_generate_wav.py +0 -0
- {pyft8-2.1.3 → pyft8-2.1.4}/tests/dev/test_loopback_performance.py +0 -0
- {pyft8-2.1.3 → pyft8-2.1.4}/tests/plot_baseline.py +0 -0
- {pyft8-2.1.3 → pyft8-2.1.4}/tests/spare.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: PyFT8
|
|
3
|
-
Version: 2.1.
|
|
3
|
+
Version: 2.1.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
|
|
@@ -34,7 +34,8 @@ PyFT8 is somewhat experimental, with a focus on demonstrating FT8 written in Pyt
|
|
|
34
34
|
- Modern programming language throughout
|
|
35
35
|
- Finds sound cards by keywords so follows them if windows moves them ...
|
|
36
36
|
|
|
37
|
-
<img width="
|
|
37
|
+
<img width="1003" height="1020" alt="image" src="https://github.com/user-attachments/assets/bf6e3f78-531a-4c9b-ab2b-b51cc04ad980" />
|
|
38
|
+
|
|
38
39
|
|
|
39
40
|
|
|
40
41
|
## Motivation
|
|
@@ -69,9 +70,11 @@ Alternatively, you can run PyFT8 without rig control; if there is no rig found,
|
|
|
69
70
|
|
|
70
71
|
The image below shows the number of decodes from PyFT8, WSJT-x V2.7.0 running in NORM mode, and FT8_lib, using the same 10 minutes of busy 20m audio that is used to test ft8_lib.
|
|
71
72
|
|
|
72
|
-
<img width="
|
|
73
|
+
<img width="640" height="480" alt="performance snapshot" src="https://github.com/user-attachments/assets/fc84702a-4d76-475b-b7fe-489f4a09deed" />
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
## Limitations![Uploading performance snapshot.png…]()
|
|
73
77
|
|
|
74
|
-
## Limitations
|
|
75
78
|
In pursuit of tight code, I've concentrated on core standard messages, leaving out some of the less-used features. The receive part of the
|
|
76
79
|
code doesn't (yet) have the full capability of the advanced decoders used in WSJT-x, and so gets fewer decodes than WSJT-x gets, depending on band conditions (on a quiet band with only good signals PyFT8 will get close to 100%).
|
|
77
80
|
|
|
@@ -8,6 +8,31 @@ from matplotlib.widgets import Slider, Button
|
|
|
8
8
|
rcParams['toolbar'] = 'None'
|
|
9
9
|
# ================== WATERFALL ======================================================
|
|
10
10
|
|
|
11
|
+
class Scrollbox:
|
|
12
|
+
def __init__(self, fig, ax, nlines = 5):
|
|
13
|
+
self.fig, self.ax = fig, ax
|
|
14
|
+
bbox = ax.get_window_extent().transformed(fig.dpi_scale_trans.inverted())
|
|
15
|
+
self.fontsize = 0.5 * bbox.height * fig.dpi / nlines
|
|
16
|
+
self.nlines = nlines
|
|
17
|
+
self.line_height = 0.9 / nlines
|
|
18
|
+
self.lines = []
|
|
19
|
+
self.clear()
|
|
20
|
+
|
|
21
|
+
def clear(self):
|
|
22
|
+
self.ax.cla()
|
|
23
|
+
self.ax.set_xticks([])
|
|
24
|
+
self.ax.set_yticks([])
|
|
25
|
+
self.ax.set_facecolor('black')
|
|
26
|
+
|
|
27
|
+
def print(self, text, color = 'white'):
|
|
28
|
+
self.clear()
|
|
29
|
+
self.lines = self.lines[-(self.nlines-1):]
|
|
30
|
+
self.lines.append({'art':None, 'text':text, 'color':color})
|
|
31
|
+
for i, line in enumerate(self.lines):
|
|
32
|
+
if line['text'] is not None:
|
|
33
|
+
line['art'] = self.ax.text(0.03,1 - self.line_height * (i+1), line['text'], color = line['color'], fontsize = self.fontsize)
|
|
34
|
+
self.fig.canvas.draw()
|
|
35
|
+
|
|
11
36
|
class Msg_box:
|
|
12
37
|
def __init__(self, fig, ax, tbin, fbin, w, h, onclick):
|
|
13
38
|
from matplotlib.patches import Rectangle
|
|
@@ -47,50 +72,48 @@ class Msg_box:
|
|
|
47
72
|
|
|
48
73
|
class Gui:
|
|
49
74
|
def __init__(self, dBgrid, hps, bpt, config, on_msg_click, on_control_click):
|
|
50
|
-
|
|
75
|
+
if config is not None:
|
|
76
|
+
self.mStation = {'c':config['station']['call'], 'g':config['station']['grid']}
|
|
51
77
|
self.on_msg_click = on_msg_click
|
|
52
78
|
self.on_control_click = on_control_click
|
|
53
79
|
self.dBgrid = dBgrid
|
|
54
80
|
self.hps, self.bpt = hps, bpt
|
|
55
81
|
self.msg_boxes = {}
|
|
56
82
|
self.decode_queue = queue.Queue()
|
|
57
|
-
self.
|
|
83
|
+
self.pmarg = 0.04
|
|
58
84
|
self.make_layout(config)
|
|
85
|
+
self.ani = FuncAnimation(self.fig, self._animate, interval = 40, frames=(100000), blit=True)
|
|
59
86
|
|
|
60
|
-
def
|
|
61
|
-
if self.simple_message_art:
|
|
62
|
-
self.simple_message_art.remove()
|
|
63
|
-
self.simple_message_art = self.fig.text(0.2,0.985, text, color = color)
|
|
64
|
-
self.fig.canvas.draw()
|
|
65
|
-
self.fig.canvas.flush_events()
|
|
66
|
-
|
|
67
|
-
def make_layout(self, config):
|
|
68
|
-
self.fig, self.ax_wf = plt.subplots(figsize=(10,10), frameon = False)
|
|
69
|
-
self.fig.canvas.manager.set_window_title('PyFT8 by G1OJS')
|
|
87
|
+
def make_layout(self, config, wf_left = 0.15, wf_top = 0.87):
|
|
70
88
|
self.plt = plt
|
|
71
|
-
plt.
|
|
72
|
-
self.
|
|
73
|
-
|
|
74
|
-
self.ax_wf.
|
|
75
|
-
|
|
89
|
+
self.fig = plt.figure(figsize = (10,10), facecolor=(.18, .71, .71, 0.4))
|
|
90
|
+
self.fig.canvas.manager.set_window_title('PyFT8 by G1OJS')
|
|
91
|
+
self.ax_wf = self.fig.add_axes([self.pmarg + wf_left, self.pmarg, 1-2*self.pmarg-wf_left, wf_top-self.pmarg])
|
|
92
|
+
self.image = self.ax_wf.imshow(self.dBgrid.T,vmax=120,vmin=90,origin='lower',interpolation='none', aspect = 'auto')
|
|
93
|
+
self.ax_wf.set_xticks([])
|
|
94
|
+
self.ax_wf.set_yticks([])
|
|
95
|
+
self.ax_console = self.fig.add_axes([self.pmarg + wf_left, wf_top, 1-2*self.pmarg - wf_left, 1-self.pmarg-wf_top])
|
|
96
|
+
self.console = Scrollbox(self.fig, self.ax_console)
|
|
97
|
+
|
|
98
|
+
if config is not None:
|
|
99
|
+
styles = {'ctrl':{'fc':'grey','c':'black'}, 'band':{'fc':'green','c':'white'}}
|
|
100
|
+
button_defs = [{'label':'CQ','style':'ctrl','data':None}, {'label':'Repeat last','style':'ctrl','data':None},
|
|
101
|
+
{'label':'Tx off','style':'ctrl','data':None}]
|
|
102
|
+
#{'label':'Averaging','style':'ctrl','data':None}]
|
|
103
|
+
for band, freq in config['bands'].items():
|
|
104
|
+
button_defs.append({'label':band,'style':'band','data':freq})
|
|
105
|
+
self._make_buttons(button_defs, styles, wf_top, 0.02, 0.1, 0.002)
|
|
106
|
+
|
|
107
|
+
def _make_buttons(self, buttons, styles, btns_top, btn_h, btn_w, sep_h):
|
|
76
108
|
self.buttons = []
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
{'label':'Tx off','style':'ctrl','data':None}]
|
|
80
|
-
#{'label':'Averaging','style':'ctrl','data':None}]
|
|
81
|
-
for band, freq in config['bands'].items():
|
|
82
|
-
control_buttons.append({'label':band,'style':'band','data':freq})
|
|
83
|
-
|
|
84
|
-
btn_axs = []
|
|
85
|
-
for i, btn in enumerate(control_buttons):
|
|
86
|
-
btn_axs.append(plt.axes([0.05, 0.9 - 0.022 * i, 0.1, 0.02]))
|
|
109
|
+
for i, btn in enumerate(buttons):
|
|
110
|
+
btn_axs = plt.axes([self.pmarg, btns_top - (i+1) * btn_h, btn_w, btn_h-sep_h])
|
|
87
111
|
style = styles[btn['style']]
|
|
88
|
-
btn_widg = Button(btn_axs
|
|
112
|
+
btn_widg = Button(btn_axs, btn['label'], color=style['fc'], hovercolor='skyblue')
|
|
89
113
|
btn_widg.data = btn['data']
|
|
90
114
|
btn_widg.on_clicked(lambda event, btn_widg=btn_widg: self.on_control_click(btn_widg))
|
|
91
115
|
self.buttons.append(btn_widg)
|
|
92
|
-
|
|
93
|
-
|
|
116
|
+
|
|
94
117
|
def add_message_box(self, message):
|
|
95
118
|
self.decode_queue.put(message)
|
|
96
119
|
|
|
@@ -11,16 +11,15 @@ from PyFT8.time_utils import global_time_utils
|
|
|
11
11
|
|
|
12
12
|
MAX_TX_START_SECONDS = 2.5
|
|
13
13
|
T_CYC = 15
|
|
14
|
-
rig = None
|
|
15
|
-
gui = None
|
|
14
|
+
rig, gui, qso, worked_before = None, None, None, None
|
|
16
15
|
|
|
17
16
|
def load_rigctrl():
|
|
18
17
|
try:
|
|
19
18
|
from PyFT8.rigctrl import Rig
|
|
20
|
-
|
|
19
|
+
console_print("Loaded Rig control")
|
|
21
20
|
return Rig()
|
|
22
21
|
except ImportError:
|
|
23
|
-
|
|
22
|
+
console_print("No Rig control found")
|
|
24
23
|
return None
|
|
25
24
|
|
|
26
25
|
def get_config(config_folder):
|
|
@@ -33,8 +32,8 @@ def get_config(config_folder):
|
|
|
33
32
|
config['bands'] = {'20m':14.074}
|
|
34
33
|
with open(ini_file, 'w') as f:
|
|
35
34
|
config.write(f)
|
|
36
|
-
|
|
37
|
-
|
|
35
|
+
console_print(f"Wrote default config to {ini_file}")
|
|
36
|
+
console_print(f"Reading config from {ini_file}")
|
|
38
37
|
config.read(ini_file)
|
|
39
38
|
|
|
40
39
|
def parse_from_adif_rec(rec, field):
|
|
@@ -97,13 +96,15 @@ class Logging:
|
|
|
97
96
|
f.write(f"<{k}:{len(v)}>{v} ")
|
|
98
97
|
f.write(f"<eor>\n")
|
|
99
98
|
self.update_worked_before(oStation['c'], time.time())
|
|
100
|
-
|
|
99
|
+
console_print(f"Logged QSO with {oStation['c']}")
|
|
101
100
|
|
|
102
101
|
|
|
103
102
|
class Message:
|
|
104
103
|
def __init__(self, candidate):
|
|
105
104
|
c = candidate
|
|
106
|
-
mycall =
|
|
105
|
+
mycall = ''
|
|
106
|
+
if qso is not None:
|
|
107
|
+
mycall = qso.mStation['c']
|
|
107
108
|
self.h0_idx, self.f0_idx, self.msg_tuple, self.msg, self.snr, self.dt, self.fHz = c.h0_idx, c.f0_idx, c.msg_tuple, c.msg, c.snr, c.dt, c.fHz
|
|
108
109
|
self.cyclestart = c.cyclestart
|
|
109
110
|
self.expire = time.time() + 29.8
|
|
@@ -111,7 +112,7 @@ class Message:
|
|
|
111
112
|
self.is_to_me = c.msg_tuple[0] == mycall
|
|
112
113
|
self.is_cq = c.msg_tuple[0].startswith('CQ')
|
|
113
114
|
gui_wb_text = ''
|
|
114
|
-
if self.is_cq:
|
|
115
|
+
if self.is_cq and worked_before is not None:
|
|
115
116
|
if c.msg_tuple[1] in worked_before:
|
|
116
117
|
gui_wb_text = f"wb: {global_time_utils.format_duration(time.time() - worked_before[c.msg_tuple[1]])}"
|
|
117
118
|
self.gui_text = f"{c.msg} {gui_wb_text}"
|
|
@@ -123,7 +124,8 @@ class Message:
|
|
|
123
124
|
class FT8_QSO:
|
|
124
125
|
def __init__(self, logging):
|
|
125
126
|
self.logging = logging
|
|
126
|
-
|
|
127
|
+
if config is not None:
|
|
128
|
+
self.mStation = {'c':config['station']['call'], 'g':config['station']['grid']}
|
|
127
129
|
self.band_info = {'b':None, 'f':0}
|
|
128
130
|
self.tx_freq = 750
|
|
129
131
|
threading.Thread(target = self._transmitter, daemon = True).start()
|
|
@@ -139,9 +141,9 @@ class FT8_QSO:
|
|
|
139
141
|
|
|
140
142
|
def set_tx_message(self, message):
|
|
141
143
|
if gui and self.band_info['b'] is None:
|
|
142
|
-
|
|
144
|
+
console_print("[PyFT8] Please select a band before transmitting", color = 'red')
|
|
143
145
|
return
|
|
144
|
-
|
|
146
|
+
console_print(f"[QSO] Set transmit message to '{message}' with tx cycle = {self.tx_cycle}")
|
|
145
147
|
self.message_to_transmit = message
|
|
146
148
|
|
|
147
149
|
def _transmitter(self):
|
|
@@ -150,7 +152,7 @@ class FT8_QSO:
|
|
|
150
152
|
if self.message_to_transmit is None:
|
|
151
153
|
continue
|
|
152
154
|
if output_device_idx is None:
|
|
153
|
-
|
|
155
|
+
console_print("No output device")
|
|
154
156
|
return
|
|
155
157
|
ct = global_time_utils.cycle_time()
|
|
156
158
|
if ct > MAX_TX_START_SECONDS:
|
|
@@ -159,7 +161,7 @@ class FT8_QSO:
|
|
|
159
161
|
if self.tx_cycle is None:
|
|
160
162
|
self.tx_cycle = global_time_utils.curr_cycle_from_time()
|
|
161
163
|
self.tx_freq = clear_frequencies[self.tx_cycle]
|
|
162
|
-
|
|
164
|
+
console_print(f"Transmitting {self.message_to_transmit} on cycle {self.tx_cycle}")
|
|
163
165
|
symbols = audio_out.create_ft8_symbols(self.message_to_transmit)
|
|
164
166
|
audio_data = audio_out.create_ft8_wave(symbols, f_base = self.tx_freq)
|
|
165
167
|
rig.PyFT8_ptt_on()
|
|
@@ -169,7 +171,8 @@ class FT8_QSO:
|
|
|
169
171
|
self.message_to_transmit = None
|
|
170
172
|
|
|
171
173
|
def log(self):
|
|
172
|
-
self.logging
|
|
174
|
+
if self.logging is not None:
|
|
175
|
+
self.logging.log(self.times, self.band_info, self.mStation, self.oStation, self.rpts)
|
|
173
176
|
|
|
174
177
|
def isReport(grid_rpt): return "+" in grid_rpt or "-" in grid_rpt
|
|
175
178
|
def isRReport(grid_rpt): return isReport(grid_rpt) and 'R' in grid_rpt
|
|
@@ -182,12 +185,14 @@ def progress_qso(clicked_message):
|
|
|
182
185
|
global qso
|
|
183
186
|
|
|
184
187
|
if time.time() - clicked_message.cyclestart['time'] > (15 + MAX_TX_START_SECONDS):
|
|
185
|
-
|
|
188
|
+
console_print("Try next cycle")
|
|
186
189
|
return
|
|
187
190
|
|
|
188
191
|
call_a, call_b, grid_rpt = clicked_message.msg_tuple
|
|
189
192
|
my_station = qso.mStation
|
|
190
193
|
reply = ""
|
|
194
|
+
msg = ' '.join(clicked_message.msg_tuple)
|
|
195
|
+
console_print(f"[QSO] Clicked on message '{msg}'")
|
|
191
196
|
|
|
192
197
|
if call_a == "CQ":
|
|
193
198
|
qso.clear()
|
|
@@ -212,7 +217,7 @@ def progress_qso(clicked_message):
|
|
|
212
217
|
if isRR73(grid_rpt):
|
|
213
218
|
reply = f"{qso.oStation['c']} {my_station['c']} 73"
|
|
214
219
|
qso.set_tx_message(reply)
|
|
215
|
-
|
|
220
|
+
|
|
216
221
|
if is73(grid_rpt) or " 73" in reply or isRR73(grid_rpt):
|
|
217
222
|
qso.times['time_off'] = time.gmtime()
|
|
218
223
|
qso.log()
|
|
@@ -221,7 +226,7 @@ def make_wav(msg, wave_output_file): # move to transmitter.py?
|
|
|
221
226
|
symbols = audio_out.create_ft8_symbols(msg)
|
|
222
227
|
audio_data = audio_out.create_ft8_wave(symbols)
|
|
223
228
|
audio_out.write_to_wave_file(audio_data, wave_output_file)
|
|
224
|
-
|
|
229
|
+
console_print(f"Created wave file {wave_output_file}")
|
|
225
230
|
|
|
226
231
|
def wait_for_keyboard():
|
|
227
232
|
import time
|
|
@@ -245,7 +250,7 @@ def on_busy_profile(busy_profile, cycle):
|
|
|
245
250
|
f0_idx, fn_idx = int(500/audio_in.df), int(fmax/audio_in.df)
|
|
246
251
|
idx = np.argmin(busy_profile[f0_idx:fn_idx])
|
|
247
252
|
clear_frequencies[cycle] = (f0_idx + idx) * audio_in.df
|
|
248
|
-
|
|
253
|
+
console_print(f"[on_busy] Set Tx freq to {clear_frequencies[cycle]:6.1f} for cycle {cycle}")
|
|
249
254
|
|
|
250
255
|
def on_control_click(btn_widg):
|
|
251
256
|
btn_text, btn_data = btn_widg.label.get_text(), btn_widg.data
|
|
@@ -255,16 +260,23 @@ def on_control_click(btn_widg):
|
|
|
255
260
|
if btn_text == "Repeat last":
|
|
256
261
|
qso.set_tx_message(qso.last_tx)
|
|
257
262
|
if btn_text == "Tx off":
|
|
263
|
+
console_print("[PyFT8] Set PTT Off")
|
|
258
264
|
rig.PyFT8_ptt_off()
|
|
259
265
|
if('m' in btn_text):
|
|
260
266
|
qso.band_info = {'b':btn_text, 'f':btn_data}
|
|
261
267
|
rig.PyFT8_set_freq_Hz(int(1000000*float(qso.band_info['f'])))
|
|
262
|
-
|
|
268
|
+
console_print(f"[PyFT8] Set band: {qso.band_info['b']} {qso.band_info['f']}")
|
|
263
269
|
|
|
264
270
|
def on_msg_click(message):
|
|
265
271
|
progress_qso(message)
|
|
266
272
|
|
|
267
|
-
#=============== CLI ========================================================================
|
|
273
|
+
#=============== CLI ========================================================================
|
|
274
|
+
def console_print(text, color = 'white'):
|
|
275
|
+
if gui is not None:
|
|
276
|
+
gui.console.print(text, color)
|
|
277
|
+
else:
|
|
278
|
+
print(text)
|
|
279
|
+
|
|
268
280
|
def cli():
|
|
269
281
|
global audio_in, audio_out, output_device_idx, rig, gui, qso, config, clear_frequencies
|
|
270
282
|
import time
|
|
@@ -282,7 +294,6 @@ def cli():
|
|
|
282
294
|
config_folder = f"{args.config_folder}".strip()
|
|
283
295
|
get_config(config_folder)
|
|
284
296
|
logging = Logging(config_folder)
|
|
285
|
-
#print(worked_before)
|
|
286
297
|
qso = FT8_QSO(logging)
|
|
287
298
|
rig = load_rigctrl()
|
|
288
299
|
|
|
@@ -303,7 +314,7 @@ def cli():
|
|
|
303
314
|
audio_in = AudioIn(3100)
|
|
304
315
|
input_device_idx = audio_in.find_device(args.inputcard_keywords.replace(' ','').split(','))
|
|
305
316
|
if not input_device_idx:
|
|
306
|
-
|
|
317
|
+
console_print("No input device")
|
|
307
318
|
else:
|
|
308
319
|
gui = None if args.no_gui else Gui(audio_in.dBgrid_main, 4, 2, config, on_msg_click, on_control_click)
|
|
309
320
|
rx = Receiver(audio_in, [200, 3100], on_decode, on_busy_profile)
|
|
@@ -275,8 +275,16 @@ class Candidate:
|
|
|
275
275
|
bits77_int = check_crc(bits91_int)
|
|
276
276
|
if(bits77_int):
|
|
277
277
|
self.msg_tuple = unpack(bits77_int)
|
|
278
|
-
self.msg =
|
|
278
|
+
self.msg = self.validate(self.msg_tuple)
|
|
279
279
|
self.decode_completed = time.time()
|
|
280
|
+
|
|
281
|
+
def validate(self, msg_tuple):
|
|
282
|
+
mt = msg_tuple
|
|
283
|
+
e = False
|
|
284
|
+
e = e or (' ' in mt[0].strip() and not mt[0].startswith('CQ'))
|
|
285
|
+
#e = e or (' ' in mt[1].strip())
|
|
286
|
+
if not e:
|
|
287
|
+
return ' '.join(self.msg_tuple)
|
|
280
288
|
|
|
281
289
|
#============== RECEIVER ===========================================================
|
|
282
290
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: PyFT8
|
|
3
|
-
Version: 2.1.
|
|
3
|
+
Version: 2.1.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
|
|
@@ -34,7 +34,8 @@ PyFT8 is somewhat experimental, with a focus on demonstrating FT8 written in Pyt
|
|
|
34
34
|
- Modern programming language throughout
|
|
35
35
|
- Finds sound cards by keywords so follows them if windows moves them ...
|
|
36
36
|
|
|
37
|
-
<img width="
|
|
37
|
+
<img width="1003" height="1020" alt="image" src="https://github.com/user-attachments/assets/bf6e3f78-531a-4c9b-ab2b-b51cc04ad980" />
|
|
38
|
+
|
|
38
39
|
|
|
39
40
|
|
|
40
41
|
## Motivation
|
|
@@ -69,9 +70,11 @@ Alternatively, you can run PyFT8 without rig control; if there is no rig found,
|
|
|
69
70
|
|
|
70
71
|
The image below shows the number of decodes from PyFT8, WSJT-x V2.7.0 running in NORM mode, and FT8_lib, using the same 10 minutes of busy 20m audio that is used to test ft8_lib.
|
|
71
72
|
|
|
72
|
-
<img width="
|
|
73
|
+
<img width="640" height="480" alt="performance snapshot" src="https://github.com/user-attachments/assets/fc84702a-4d76-475b-b7fe-489f4a09deed" />
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
## Limitations![Uploading performance snapshot.png…]()
|
|
73
77
|
|
|
74
|
-
## Limitations
|
|
75
78
|
In pursuit of tight code, I've concentrated on core standard messages, leaving out some of the less-used features. The receive part of the
|
|
76
79
|
code doesn't (yet) have the full capability of the advanced decoders used in WSJT-x, and so gets fewer decodes than WSJT-x gets, depending on band conditions (on a quiet band with only good signals PyFT8 will get close to 100%).
|
|
77
80
|
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
MANIFEST.in
|
|
3
|
+
README.md
|
|
4
|
+
pyproject.toml
|
|
5
|
+
PyFT8/__init__.py
|
|
6
|
+
PyFT8/gui.py
|
|
7
|
+
PyFT8/pyft8.py
|
|
8
|
+
PyFT8/receiver.py
|
|
9
|
+
PyFT8/rigctrl.py
|
|
10
|
+
PyFT8/time_utils.py
|
|
11
|
+
PyFT8/transmitter.py
|
|
12
|
+
PyFT8.egg-info/PKG-INFO
|
|
13
|
+
PyFT8.egg-info/SOURCES.txt
|
|
14
|
+
PyFT8.egg-info/dependency_links.txt
|
|
15
|
+
PyFT8.egg-info/entry_points.txt
|
|
16
|
+
PyFT8.egg-info/requires.txt
|
|
17
|
+
PyFT8.egg-info/top_level.txt
|
|
18
|
+
tests/plot_baseline.py
|
|
19
|
+
tests/spare.py
|
|
20
|
+
tests/test_batch_and_live.py
|
|
21
|
+
tests/dev/osd.py
|
|
22
|
+
tests/dev/test_generate_wav.py
|
|
23
|
+
tests/dev/test_loopback_performance.py
|
|
@@ -15,7 +15,8 @@ PyFT8 is somewhat experimental, with a focus on demonstrating FT8 written in Pyt
|
|
|
15
15
|
- Modern programming language throughout
|
|
16
16
|
- Finds sound cards by keywords so follows them if windows moves them ...
|
|
17
17
|
|
|
18
|
-
<img width="
|
|
18
|
+
<img width="1003" height="1020" alt="image" src="https://github.com/user-attachments/assets/bf6e3f78-531a-4c9b-ab2b-b51cc04ad980" />
|
|
19
|
+
|
|
19
20
|
|
|
20
21
|
|
|
21
22
|
## Motivation
|
|
@@ -50,9 +51,11 @@ Alternatively, you can run PyFT8 without rig control; if there is no rig found,
|
|
|
50
51
|
|
|
51
52
|
The image below shows the number of decodes from PyFT8, WSJT-x V2.7.0 running in NORM mode, and FT8_lib, using the same 10 minutes of busy 20m audio that is used to test ft8_lib.
|
|
52
53
|
|
|
53
|
-
<img width="
|
|
54
|
+
<img width="640" height="480" alt="performance snapshot" src="https://github.com/user-attachments/assets/fc84702a-4d76-475b-b7fe-489f4a09deed" />
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
## Limitations![Uploading performance snapshot.png…]()
|
|
54
58
|
|
|
55
|
-
## Limitations
|
|
56
59
|
In pursuit of tight code, I've concentrated on core standard messages, leaving out some of the less-used features. The receive part of the
|
|
57
60
|
code doesn't (yet) have the full capability of the advanced decoders used in WSJT-x, and so gets fewer decodes than WSJT-x gets, depending on band conditions (on a quiet band with only good signals PyFT8 will get close to 100%).
|
|
58
61
|
|
|
@@ -5,6 +5,7 @@ import pickle
|
|
|
5
5
|
import threading
|
|
6
6
|
from PyFT8.receiver import Receiver, AudioIn
|
|
7
7
|
from PyFT8.gui import Gui
|
|
8
|
+
from PyFT8.pyft8 import Message
|
|
8
9
|
|
|
9
10
|
class Wsjtx_all_tailer:
|
|
10
11
|
|
|
@@ -37,6 +38,7 @@ class Wsjtx_all_tailer:
|
|
|
37
38
|
|
|
38
39
|
|
|
39
40
|
data_folder = "C:/Users/drala/Documents/Projects/GitHub/PyFT8/tests/data/ft8_lib_20m_busy"
|
|
41
|
+
wav_folder = "C:/Users/drala/Documents/Projects/GitHub/ft8_lib/test/wav/20m_busy"
|
|
40
42
|
|
|
41
43
|
global decodes, py_times, ws_times, decodes
|
|
42
44
|
decodes, py_times, ws_times = [], [], []
|
|
@@ -52,11 +54,10 @@ def get_cumulative_from_text_files(i0, i1, postfix):
|
|
|
52
54
|
return times
|
|
53
55
|
|
|
54
56
|
def on_decode(c):
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
print(
|
|
59
|
-
decodes.append(decode_pack)
|
|
57
|
+
message = Message(c)
|
|
58
|
+
if gui:
|
|
59
|
+
gui.add_message_box(message)
|
|
60
|
+
print(message.wsjtx_screen_format())
|
|
60
61
|
py_times.append(time.time() - t_start)
|
|
61
62
|
|
|
62
63
|
def batch_test(i0, i1):
|
|
@@ -64,10 +65,10 @@ def batch_test(i0, i1):
|
|
|
64
65
|
global t_start, gui
|
|
65
66
|
wav_files = []
|
|
66
67
|
for idx in range(i0, i1):
|
|
67
|
-
wav_files.append(f"{
|
|
68
|
+
wav_files.append(f"{wav_folder}/test_{idx:02d}.wav")
|
|
68
69
|
audio_in = AudioIn(3100, wav_files)
|
|
69
|
-
gui = Gui(audio_in.dBgrid_main, 4, 2,
|
|
70
|
-
rx = Receiver(audio_in, [200, 3100], on_decode)
|
|
70
|
+
gui = Gui(audio_in.dBgrid_main, 4, 2, None, None, None)
|
|
71
|
+
rx = Receiver(audio_in, [200, 3100], on_decode, None)
|
|
71
72
|
audio_in.start_wav_load()
|
|
72
73
|
t_start = time.time()
|
|
73
74
|
with open('baseline.pkl', 'rb') as f:
|
|
@@ -104,8 +105,8 @@ def live_test():
|
|
|
104
105
|
from matplotlib.animation import FuncAnimation
|
|
105
106
|
global t_start, gui
|
|
106
107
|
audio_in = AudioIn(3100)
|
|
107
|
-
gui = Gui(audio_in.dBgrid_main, 4, 2,
|
|
108
|
-
rx = Receiver(audio_in, [200, 3100], on_decode)
|
|
108
|
+
gui = Gui(audio_in.dBgrid_main, 4, 2, config, None, None)
|
|
109
|
+
rx = Receiver(audio_in, [200, 3100], on_decode, None)
|
|
109
110
|
t_start = time.time()
|
|
110
111
|
input_device_idx = audio_in.find_device(["Cable", "Out"])
|
|
111
112
|
audio_in.start_streamed_audio(input_device_idx)
|
|
@@ -133,7 +134,6 @@ def live_test():
|
|
|
133
134
|
gui.plt.show()
|
|
134
135
|
|
|
135
136
|
#live_test()
|
|
136
|
-
|
|
137
137
|
batch_test(1,39)
|
|
138
138
|
|
|
139
139
|
|
|
@@ -1,61 +0,0 @@
|
|
|
1
|
-
LICENSE
|
|
2
|
-
MANIFEST.in
|
|
3
|
-
README.md
|
|
4
|
-
pyproject.toml
|
|
5
|
-
PyFT8/__init__.py
|
|
6
|
-
PyFT8/gui.py
|
|
7
|
-
PyFT8/pyft8.py
|
|
8
|
-
PyFT8/receiver.py
|
|
9
|
-
PyFT8/rigctrl.py
|
|
10
|
-
PyFT8/time_utils.py
|
|
11
|
-
PyFT8/transmitter.py
|
|
12
|
-
PyFT8.egg-info/PKG-INFO
|
|
13
|
-
PyFT8.egg-info/SOURCES.txt
|
|
14
|
-
PyFT8.egg-info/dependency_links.txt
|
|
15
|
-
PyFT8.egg-info/entry_points.txt
|
|
16
|
-
PyFT8.egg-info/requires.txt
|
|
17
|
-
PyFT8.egg-info/top_level.txt
|
|
18
|
-
tests/plot_baseline.py
|
|
19
|
-
tests/spare.py
|
|
20
|
-
tests/test_batch_and_live.py
|
|
21
|
-
tests/data/ft8_lib_20m_busy/test_01.wav
|
|
22
|
-
tests/data/ft8_lib_20m_busy/test_02.wav
|
|
23
|
-
tests/data/ft8_lib_20m_busy/test_03.wav
|
|
24
|
-
tests/data/ft8_lib_20m_busy/test_04.wav
|
|
25
|
-
tests/data/ft8_lib_20m_busy/test_05.wav
|
|
26
|
-
tests/data/ft8_lib_20m_busy/test_06.wav
|
|
27
|
-
tests/data/ft8_lib_20m_busy/test_07.wav
|
|
28
|
-
tests/data/ft8_lib_20m_busy/test_08.wav
|
|
29
|
-
tests/data/ft8_lib_20m_busy/test_09.wav
|
|
30
|
-
tests/data/ft8_lib_20m_busy/test_10.wav
|
|
31
|
-
tests/data/ft8_lib_20m_busy/test_11.wav
|
|
32
|
-
tests/data/ft8_lib_20m_busy/test_12.wav
|
|
33
|
-
tests/data/ft8_lib_20m_busy/test_13.wav
|
|
34
|
-
tests/data/ft8_lib_20m_busy/test_14.wav
|
|
35
|
-
tests/data/ft8_lib_20m_busy/test_15.wav
|
|
36
|
-
tests/data/ft8_lib_20m_busy/test_16.wav
|
|
37
|
-
tests/data/ft8_lib_20m_busy/test_17.wav
|
|
38
|
-
tests/data/ft8_lib_20m_busy/test_18.wav
|
|
39
|
-
tests/data/ft8_lib_20m_busy/test_19.wav
|
|
40
|
-
tests/data/ft8_lib_20m_busy/test_20.wav
|
|
41
|
-
tests/data/ft8_lib_20m_busy/test_21.wav
|
|
42
|
-
tests/data/ft8_lib_20m_busy/test_22.wav
|
|
43
|
-
tests/data/ft8_lib_20m_busy/test_23.wav
|
|
44
|
-
tests/data/ft8_lib_20m_busy/test_24.wav
|
|
45
|
-
tests/data/ft8_lib_20m_busy/test_25.wav
|
|
46
|
-
tests/data/ft8_lib_20m_busy/test_26.wav
|
|
47
|
-
tests/data/ft8_lib_20m_busy/test_27.wav
|
|
48
|
-
tests/data/ft8_lib_20m_busy/test_28.wav
|
|
49
|
-
tests/data/ft8_lib_20m_busy/test_29.wav
|
|
50
|
-
tests/data/ft8_lib_20m_busy/test_30.wav
|
|
51
|
-
tests/data/ft8_lib_20m_busy/test_31.wav
|
|
52
|
-
tests/data/ft8_lib_20m_busy/test_32.wav
|
|
53
|
-
tests/data/ft8_lib_20m_busy/test_33.wav
|
|
54
|
-
tests/data/ft8_lib_20m_busy/test_34.wav
|
|
55
|
-
tests/data/ft8_lib_20m_busy/test_35.wav
|
|
56
|
-
tests/data/ft8_lib_20m_busy/test_36.wav
|
|
57
|
-
tests/data/ft8_lib_20m_busy/test_37.wav
|
|
58
|
-
tests/data/ft8_lib_20m_busy/test_38.wav
|
|
59
|
-
tests/dev/osd.py
|
|
60
|
-
tests/dev/test_generate_wav.py
|
|
61
|
-
tests/dev/test_loopback_performance.py
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
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
|