PyFT8 3.6.6__tar.gz → 3.7.1__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.6 → pyft8-3.7.1}/PKG-INFO +24 -2
- {pyft8-3.6.6 → pyft8-3.7.1}/PyFT8/gui.py +36 -47
- {pyft8-3.6.6 → pyft8-3.7.1}/PyFT8/message_broker.py +1 -2
- {pyft8-3.6.6 → pyft8-3.7.1}/PyFT8/pyft8.py +2 -2
- {pyft8-3.6.6 → pyft8-3.7.1}/PyFT8/receiver.py +1 -1
- {pyft8-3.6.6 → pyft8-3.7.1}/PyFT8.egg-info/PKG-INFO +24 -2
- {pyft8-3.6.6 → pyft8-3.7.1}/README.md +23 -1
- {pyft8-3.6.6 → pyft8-3.7.1}/pyproject.toml +1 -1
- {pyft8-3.6.6 → pyft8-3.7.1}/LICENSE +0 -0
- {pyft8-3.6.6 → pyft8-3.7.1}/MANIFEST.in +0 -0
- {pyft8-3.6.6 → pyft8-3.7.1}/PyFT8/__init__.py +0 -0
- {pyft8-3.6.6 → pyft8-3.7.1}/PyFT8/databases.py +0 -0
- {pyft8-3.6.6 → pyft8-3.7.1}/PyFT8/osd.py +0 -0
- {pyft8-3.6.6 → pyft8-3.7.1}/PyFT8/pskreporter.py +0 -0
- {pyft8-3.6.6 → pyft8-3.7.1}/PyFT8/qso_manager.py +0 -0
- {pyft8-3.6.6 → pyft8-3.7.1}/PyFT8/rigctrl.py +0 -0
- {pyft8-3.6.6 → pyft8-3.7.1}/PyFT8/time_utils.py +0 -0
- {pyft8-3.6.6 → pyft8-3.7.1}/PyFT8/transmitter.py +0 -0
- {pyft8-3.6.6 → pyft8-3.7.1}/PyFT8.egg-info/SOURCES.txt +0 -0
- {pyft8-3.6.6 → pyft8-3.7.1}/PyFT8.egg-info/dependency_links.txt +0 -0
- {pyft8-3.6.6 → pyft8-3.7.1}/PyFT8.egg-info/entry_points.txt +0 -0
- {pyft8-3.6.6 → pyft8-3.7.1}/PyFT8.egg-info/requires.txt +0 -0
- {pyft8-3.6.6 → pyft8-3.7.1}/PyFT8.egg-info/top_level.txt +0 -0
- {pyft8-3.6.6 → pyft8-3.7.1}/setup.cfg +0 -0
- {pyft8-3.6.6 → pyft8-3.7.1}/tests/misc/CQ AAAA.py +0 -0
- {pyft8-3.6.6 → pyft8-3.7.1}/tests/misc/animation.py +0 -0
- {pyft8-3.6.6 → pyft8-3.7.1}/tests/misc/bits_loopback.py +0 -0
- {pyft8-3.6.6 → pyft8-3.7.1}/tests/misc/bits_loopback_short.py +0 -0
- {pyft8-3.6.6 → pyft8-3.7.1}/tests/misc/osd.py +0 -0
- {pyft8-3.6.6 → pyft8-3.7.1}/tests/misc/symbols_loopback.py +0 -0
- {pyft8-3.6.6 → pyft8-3.7.1}/tests/misc/test_generate_wav.py +0 -0
- {pyft8-3.6.6 → pyft8-3.7.1}/tests/misc/test_loopback_performance.py +0 -0
- {pyft8-3.6.6 → pyft8-3.7.1}/tests/misc/text.py +0 -0
- {pyft8-3.6.6 → pyft8-3.7.1}/tests/misc/view_worked_before.py +0 -0
- {pyft8-3.6.6 → pyft8-3.7.1}/tests/pipeline/Fine tuning compare IOS_PC.py +0 -0
- {pyft8-3.6.6 → pyft8-3.7.1}/tests/pipeline/Targetted tests.py +0 -0
- {pyft8-3.6.6 → pyft8-3.7.1}/tests/pipeline/test_01.wav +0 -0
- {pyft8-3.6.6 → pyft8-3.7.1}/tests/play_wavs.py +0 -0
- {pyft8-3.6.6 → pyft8-3.7.1}/tests/plot_decode_times.py +0 -0
- {pyft8-3.6.6 → pyft8-3.7.1}/tests/spare.py +0 -0
- {pyft8-3.6.6 → pyft8-3.7.1}/tests/spare2.py +0 -0
- {pyft8-3.6.6 → pyft8-3.7.1}/tests/test_batch_and_live.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: PyFT8
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.7.1
|
|
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
|
|
@@ -23,7 +23,7 @@ Dynamic: license-file
|
|
|
23
23
|
# PyFT8 [](https://pepy.tech/projects/pyft8) [](https://pepy.tech/projects/pyft8) [PyPI link](https://pypi.org/project/PyFT8/)
|
|
24
24
|
# All-Python FT8 Transceiver GUI / Command Line Modem
|
|
25
25
|
|
|
26
|
-
This repository contains the source code for PyFT8, an all-Python open source FT8 transceiver that you can run as a basic GUI or from the command line to receive and transmit. Decoding performance (number of decodes) is
|
|
26
|
+
This repository contains the source code for PyFT8, an all-Python open source FT8 transceiver that you can run as a basic GUI or from the command line to receive and transmit. Decoding performance (number of decodes) is very close to that of WSJT-x 2.7.0 running in FAST mode.
|
|
27
27
|
|
|
28
28
|
PyFT8 is somewhat experimental, with a focus on demonstrating FT8 written in Python, but can be used as a standalone replacement for WSJT-x and other software.
|
|
29
29
|
|
|
@@ -116,6 +116,28 @@ The image below shows the number of decodes from PyFT8, WSJT-x V2.7.0 running in
|
|
|
116
116
|
|
|
117
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
|
+
## Decoding approach
|
|
120
|
+
The approach to decoding, as at v3.7.0, is as follows.
|
|
121
|
+
|
|
122
|
+
- As audio arrives
|
|
123
|
+
- FFT to a spectrum 4 times per symbol duration & assemble a time-frequency grid with 2 bins per tone.
|
|
124
|
+
- Collect all audio samples for later full cycle FFT
|
|
125
|
+
- Search the time frequency grid for candidates based on Costas array sync score (using middle Costas block)
|
|
126
|
+
- Send candidates for decode using symbol & tone points drawn directly from the search grid to get early decodes using limited LDPC cycles.
|
|
127
|
+
|
|
128
|
+
This often decodes around three quarters of the candidates decoded by WSJT-x, with the steps below getting most of the rest:
|
|
129
|
+
|
|
130
|
+
- For candidates that haven't decoded:
|
|
131
|
+
- FFT the full cycle audio, downsample to 200 samples per second & shift the signal to f = 0
|
|
132
|
+
- Explore fine time and frequency offsets to maximise the Costas sync score
|
|
133
|
+
- Check for bad candidates (count maxima of Costas tones)
|
|
134
|
+
- Extract the LLR & wait for decoder to pick up the candidate
|
|
135
|
+
- For candidates awaiting decode from above:
|
|
136
|
+
- Try more extensive LDPC
|
|
137
|
+
- Try several 'A Priori' masks and input to more LDPC attempts, recording the output LLR on failures
|
|
138
|
+
- Try Ordered Statistics Decoding on the base LLR
|
|
139
|
+
- Try Ordered Statistics Decoding on the LLRs output from the AP+LDPC runs above
|
|
140
|
+
|
|
119
141
|
## Limitations
|
|
120
142
|
PyFT8 doesn't decode / encode *all* message types. The table below shows which are handled.
|
|
121
143
|
|
|
@@ -25,13 +25,12 @@ def add_my_axes(fig, pos):
|
|
|
25
25
|
ax.set_facecolor(TEXT_BACKGROUND_COLOR)
|
|
26
26
|
return ax
|
|
27
27
|
|
|
28
|
-
MESSAGE_TYPES = {'generic':{'bg':'blue', 'fg':'white', 'alpha':0.
|
|
29
|
-
'from_me': {'bg':'yellow', 'fg':'black', 'alpha':0.
|
|
28
|
+
MESSAGE_TYPES = {'generic':{'bg':'blue', 'fg':'white', 'alpha':0.4}, 'CQ':{'bg':'green', 'fg':'white', 'alpha':0.7},
|
|
29
|
+
'from_me': {'bg':'yellow', 'fg':'black', 'alpha':0.9}, 'to_me':{'bg':'red', 'fg':'white', 'alpha':0.9}}
|
|
30
30
|
class Msg_box:
|
|
31
|
-
def __init__(self, fig, ax,
|
|
31
|
+
def __init__(self, fig, ax, w, h):
|
|
32
32
|
from matplotlib.patches import Rectangle
|
|
33
33
|
self.fig, self.ax = fig, ax
|
|
34
|
-
message_type_params = MESSAGE_TYPES[message['message_type']]
|
|
35
34
|
rect = Rectangle((0, 0), w, h, edgecolor='lime', lw=2)
|
|
36
35
|
self.patch = self.ax.add_patch(rect)
|
|
37
36
|
self.patch.set_visible(False)
|
|
@@ -39,7 +38,6 @@ class Msg_box:
|
|
|
39
38
|
self.text_inst.set_visible(False)
|
|
40
39
|
self.text_inst.set_fontfamily('monospace')
|
|
41
40
|
self.artists = [self.patch, self.text_inst]
|
|
42
|
-
self.set_props(x, y, message)
|
|
43
41
|
|
|
44
42
|
def set_props(self, x, y, message):
|
|
45
43
|
self.xy = (x, y)
|
|
@@ -177,10 +175,10 @@ class Gui:
|
|
|
177
175
|
self.cid = self.fig.canvas.mpl_connect('button_press_event', self._oncanvasclick)
|
|
178
176
|
|
|
179
177
|
def main_loop(self):
|
|
180
|
-
last_ptr = 0
|
|
181
178
|
self.plt.show(block = False)
|
|
182
|
-
target_update_time = 0.
|
|
179
|
+
target_update_time = 0.15
|
|
183
180
|
while True:
|
|
181
|
+
self.fig.canvas.flush_events() # for clicks
|
|
184
182
|
tstart_wf_redraw = time_utils.time()
|
|
185
183
|
self.image.set_data(self.waterfall_data['data'])
|
|
186
184
|
if self.needs_redraw:
|
|
@@ -188,42 +186,29 @@ class Gui:
|
|
|
188
186
|
plt.pause(0.01)
|
|
189
187
|
else:
|
|
190
188
|
self.ax_wf.draw_artist(self.image)
|
|
191
|
-
|
|
189
|
+
self.fig.canvas.flush_events() # for clicks
|
|
192
190
|
for mb in self.msg_boxes:
|
|
193
|
-
mb.
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
#self.fig.canvas.update()
|
|
191
|
+
if mb.patch.get_visible():
|
|
192
|
+
mb.draw()
|
|
193
|
+
self.fig.canvas.flush_events() # for clicks
|
|
194
|
+
self.fig.canvas.blit(self.ax_wf.bbox) #self.fig.canvas.update()
|
|
197
195
|
self.fig.canvas.flush_events()
|
|
198
|
-
#print(f"Updated at {time_utils.cycle_time():6.2f}s")
|
|
199
196
|
tdelay = target_update_time - (time_utils.time() - tstart_wf_redraw)
|
|
200
|
-
#print(tdelay)
|
|
201
197
|
if tdelay > 0.01:
|
|
202
198
|
time_utils.sleep(tdelay)
|
|
203
199
|
|
|
204
|
-
def before_search(self, curr_cycle):
|
|
205
|
-
self._hide_msg_boxes(curr_cycle) # hide boxes overlapping new decodes
|
|
200
|
+
def before_search(self, curr_cycle): # would be better with a local trigger
|
|
201
|
+
#self._hide_msg_boxes(curr_cycle) # hide boxes overlapping new decodes
|
|
206
202
|
self._hide_msg_boxes(curr_cycle = None) # hide all boxes
|
|
207
|
-
|
|
208
|
-
def after_search(self, curr_cycle):
|
|
209
|
-
self._refresh_hearing()
|
|
210
|
-
self._refresh_band_buttons()
|
|
211
203
|
self._refresh_home_panel()
|
|
212
|
-
|
|
204
|
+
self._refresh_band_buttons()
|
|
205
|
+
self._refresh_hearing()
|
|
213
206
|
|
|
214
207
|
def display_message(self, message):
|
|
208
|
+
mb = self._get_message_box()
|
|
215
209
|
x = int(message['t0'] / self.waterfall_data['dt'] + message['their_tx_cycle'] * self.waterfall_data['pixels_per_cycle'])
|
|
216
210
|
y = int(message['fHz'] / self.waterfall_data['df'])
|
|
217
|
-
mb
|
|
218
|
-
for mb1 in self.msg_boxes:
|
|
219
|
-
if not mb1.patch.get_visible():
|
|
220
|
-
mb1.set_props(x, y, message)
|
|
221
|
-
mb = mb1
|
|
222
|
-
break
|
|
223
|
-
if not mb:
|
|
224
|
-
mb = Msg_box(self.fig, self.ax_wf, x, y, self.waterfall_data['sig_w'], self.waterfall_data['sig_h'], message)
|
|
225
|
-
self.msg_boxes.append(mb)
|
|
226
|
-
#print(f"{len(self.msg_boxes)} message boxes")
|
|
211
|
+
mb.set_props(x, y, message)
|
|
227
212
|
|
|
228
213
|
def update_message(self, display_text, update_dict):
|
|
229
214
|
for mb in self.msg_boxes:
|
|
@@ -252,17 +237,35 @@ class Gui:
|
|
|
252
237
|
color = row_text['color'])
|
|
253
238
|
self.needs_redraw = True
|
|
254
239
|
|
|
240
|
+
def _get_message_box(self):
|
|
241
|
+
mb = None
|
|
242
|
+
for mb1 in self.msg_boxes:
|
|
243
|
+
if not mb1.patch.get_visible():
|
|
244
|
+
mb = mb1
|
|
245
|
+
break
|
|
246
|
+
if not mb:
|
|
247
|
+
mb = Msg_box(self.fig, self.ax_wf, self.waterfall_data['sig_w'], self.waterfall_data['sig_h'])
|
|
248
|
+
self.msg_boxes.append(mb)
|
|
249
|
+
return mb
|
|
250
|
+
|
|
255
251
|
def _set_band(self, band):
|
|
256
252
|
if band in self.configured_bands:
|
|
257
253
|
self.band_info = {'current_band':band, 'fMHz':self.configured_bands[band], 'time_set':time_utils.time()}
|
|
258
254
|
self.console_print(f"[PyFT8] Set band: {self.band_info['current_band']} {self.band_info['fMHz']}")
|
|
259
|
-
self.
|
|
255
|
+
for mb in self.msg_boxes:
|
|
256
|
+
mb.hide()
|
|
257
|
+
mb.remove()
|
|
258
|
+
self.msg_boxes = []
|
|
259
|
+
self._refresh_home_panel()
|
|
260
|
+
self._refresh_band_buttons()
|
|
261
|
+
self._refresh_hearing()
|
|
262
|
+
self.needs_redraw = True
|
|
260
263
|
|
|
261
264
|
def _oncanvasclick(self, clickargs):
|
|
262
265
|
if clickargs.inaxes is self.ax_wf:
|
|
263
266
|
for mb in self.msg_boxes:
|
|
264
267
|
if mb.contains(clickargs.x, clickargs.y):
|
|
265
|
-
self.console_print(f"[GUI] Clicked on message '{mb.message['msg_text']}
|
|
268
|
+
self.console_print(f"[GUI] Clicked on message '{mb.message['msg_text']}")
|
|
266
269
|
self.qso_manager.on_click({'action':"MESSAGE_CLICK", 'message':mb.message})
|
|
267
270
|
return
|
|
268
271
|
for bb in self.button_boxes:
|
|
@@ -271,7 +274,6 @@ class Gui:
|
|
|
271
274
|
self.needs_redraw = True
|
|
272
275
|
self._set_band(bb.id)
|
|
273
276
|
self.qso_manager.on_click({'action':"SET_BAND", 'fMHz':self.configured_bands[bb.id]})
|
|
274
|
-
self.after_search(curr_cycle = None)
|
|
275
277
|
else:
|
|
276
278
|
action = ["CQ", "RPT_LAST", "TX_OFF"][['CQ', 'Repeat last', 'Tx off'].index(bb.id)]
|
|
277
279
|
self.qso_manager.on_click({'action':action})
|
|
@@ -281,19 +283,6 @@ class Gui:
|
|
|
281
283
|
for mb in to_hide:
|
|
282
284
|
mb.hide()
|
|
283
285
|
self.needs_redraw = True
|
|
284
|
-
|
|
285
|
-
def _clear_msg_boxes(self, curr_cycle = None):
|
|
286
|
-
to_remove = [mb for mb in self.msg_boxes if (mb.cycle == curr_cycle) or (curr_cycle is None)]
|
|
287
|
-
self.msg_boxes = [mb for mb in self.msg_boxes if (mb.cycle != curr_cycle) and not (curr_cycle is None)]
|
|
288
|
-
for mb in to_remove:
|
|
289
|
-
mb.hide()
|
|
290
|
-
mb.remove()
|
|
291
|
-
self.needs_redraw = True
|
|
292
|
-
|
|
293
|
-
def _text_row(self, ax, x, y, text = '', color = 'white', **args):
|
|
294
|
-
art = ax.text(x, y, text, color = color)
|
|
295
|
-
art.set_fontfamily('monospace')
|
|
296
|
-
return art
|
|
297
286
|
|
|
298
287
|
def _refresh_home_panel(self):
|
|
299
288
|
current_band = self.band_info['current_band']
|
|
@@ -34,7 +34,7 @@ class Broker():
|
|
|
34
34
|
self.on_decode(message_dict)
|
|
35
35
|
m = message_dict
|
|
36
36
|
screen_format = f"{cyclestart_string} {their_snr} {m['dt']:4.1f} {m['fHz']:4.0f} ~ {hail} {their_call} {grid_rpt}"
|
|
37
|
-
print(f"{screen_format:50s} decoded@ {m['decode_completed']:5.1f}s, dec = {m['decode_status']}")
|
|
37
|
+
print(f"{screen_format:50s} decoded@ {m['decode_completed'] %15:5.1f}s, dec = {m['decode_status']}")
|
|
38
38
|
self.message_queue_non_time_critical.put(message_dict)
|
|
39
39
|
|
|
40
40
|
def _process_message_ntc(self):
|
|
@@ -55,7 +55,6 @@ class Broker():
|
|
|
55
55
|
new_display_text = f"{m['display_text']} {hearing_me} {wb_text} {geo_text}"
|
|
56
56
|
self.gui.update_message( m['display_text'], {'hearing_me':hearing_me, 'wb_text':wb_text,
|
|
57
57
|
'geo_text':geo_text, 'display_text':new_display_text } )
|
|
58
|
-
|
|
59
58
|
if band_info:
|
|
60
59
|
if m['their_call'] != 'not':
|
|
61
60
|
if self.history:
|
|
@@ -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.
|
|
14
|
+
VER = '3.7.1'
|
|
15
15
|
PSKR_REFRESH_MINS = 20
|
|
16
16
|
message_broker = None
|
|
17
17
|
|
|
@@ -136,7 +136,7 @@ def cli():
|
|
|
136
136
|
qso_manager = QSO_manager(message_broker, rig_control, console_print)
|
|
137
137
|
message_broker.gui.register_qso_manager(qso_manager)
|
|
138
138
|
message_broker.rx.register_before_search(message_broker.gui.before_search)
|
|
139
|
-
message_broker.rx.register_after_search(message_broker.gui.after_search)
|
|
139
|
+
#message_broker.rx.register_after_search(message_broker.gui.after_search)
|
|
140
140
|
message_broker.history.incorporate_log_data(qso_manager.adif_logging.cache)
|
|
141
141
|
message_broker.history.start_collect_new()
|
|
142
142
|
|
|
@@ -592,7 +592,7 @@ class Receiver():
|
|
|
592
592
|
'msg_text':f"{hail} {their_call} {grid_rpt}",
|
|
593
593
|
'their_snr':f"{c.snr:+03d}", 'their_tx_cycle':c.origin['odd_even'],
|
|
594
594
|
't0':c.origin['t0'], 'dt':c.origin['t0'] - 0.5, 'fHz':c.origin['f0'],
|
|
595
|
-
'decode_status':c.decode_status, 'decode_completed':time_utils.
|
|
595
|
+
'decode_status':c.decode_status, 'decode_completed':time_utils.time()}
|
|
596
596
|
self.process_message(message_dict)
|
|
597
597
|
|
|
598
598
|
if len(to_decode):
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: PyFT8
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.7.1
|
|
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
|
|
@@ -23,7 +23,7 @@ Dynamic: license-file
|
|
|
23
23
|
# PyFT8 [](https://pepy.tech/projects/pyft8) [](https://pepy.tech/projects/pyft8) [PyPI link](https://pypi.org/project/PyFT8/)
|
|
24
24
|
# All-Python FT8 Transceiver GUI / Command Line Modem
|
|
25
25
|
|
|
26
|
-
This repository contains the source code for PyFT8, an all-Python open source FT8 transceiver that you can run as a basic GUI or from the command line to receive and transmit. Decoding performance (number of decodes) is
|
|
26
|
+
This repository contains the source code for PyFT8, an all-Python open source FT8 transceiver that you can run as a basic GUI or from the command line to receive and transmit. Decoding performance (number of decodes) is very close to that of WSJT-x 2.7.0 running in FAST mode.
|
|
27
27
|
|
|
28
28
|
PyFT8 is somewhat experimental, with a focus on demonstrating FT8 written in Python, but can be used as a standalone replacement for WSJT-x and other software.
|
|
29
29
|
|
|
@@ -116,6 +116,28 @@ The image below shows the number of decodes from PyFT8, WSJT-x V2.7.0 running in
|
|
|
116
116
|
|
|
117
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
|
+
## Decoding approach
|
|
120
|
+
The approach to decoding, as at v3.7.0, is as follows.
|
|
121
|
+
|
|
122
|
+
- As audio arrives
|
|
123
|
+
- FFT to a spectrum 4 times per symbol duration & assemble a time-frequency grid with 2 bins per tone.
|
|
124
|
+
- Collect all audio samples for later full cycle FFT
|
|
125
|
+
- Search the time frequency grid for candidates based on Costas array sync score (using middle Costas block)
|
|
126
|
+
- Send candidates for decode using symbol & tone points drawn directly from the search grid to get early decodes using limited LDPC cycles.
|
|
127
|
+
|
|
128
|
+
This often decodes around three quarters of the candidates decoded by WSJT-x, with the steps below getting most of the rest:
|
|
129
|
+
|
|
130
|
+
- For candidates that haven't decoded:
|
|
131
|
+
- FFT the full cycle audio, downsample to 200 samples per second & shift the signal to f = 0
|
|
132
|
+
- Explore fine time and frequency offsets to maximise the Costas sync score
|
|
133
|
+
- Check for bad candidates (count maxima of Costas tones)
|
|
134
|
+
- Extract the LLR & wait for decoder to pick up the candidate
|
|
135
|
+
- For candidates awaiting decode from above:
|
|
136
|
+
- Try more extensive LDPC
|
|
137
|
+
- Try several 'A Priori' masks and input to more LDPC attempts, recording the output LLR on failures
|
|
138
|
+
- Try Ordered Statistics Decoding on the base LLR
|
|
139
|
+
- Try Ordered Statistics Decoding on the LLRs output from the AP+LDPC runs above
|
|
140
|
+
|
|
119
141
|
## Limitations
|
|
120
142
|
PyFT8 doesn't decode / encode *all* message types. The table below shows which are handled.
|
|
121
143
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# PyFT8 [](https://pepy.tech/projects/pyft8) [](https://pepy.tech/projects/pyft8) [PyPI link](https://pypi.org/project/PyFT8/)
|
|
2
2
|
# All-Python FT8 Transceiver GUI / Command Line Modem
|
|
3
3
|
|
|
4
|
-
This repository contains the source code for PyFT8, an all-Python open source FT8 transceiver that you can run as a basic GUI or from the command line to receive and transmit. Decoding performance (number of decodes) is
|
|
4
|
+
This repository contains the source code for PyFT8, an all-Python open source FT8 transceiver that you can run as a basic GUI or from the command line to receive and transmit. Decoding performance (number of decodes) is very close to that of WSJT-x 2.7.0 running in FAST mode.
|
|
5
5
|
|
|
6
6
|
PyFT8 is somewhat experimental, with a focus on demonstrating FT8 written in Python, but can be used as a standalone replacement for WSJT-x and other software.
|
|
7
7
|
|
|
@@ -94,6 +94,28 @@ The image below shows the number of decodes from PyFT8, WSJT-x V2.7.0 running in
|
|
|
94
94
|
|
|
95
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
|
+
## Decoding approach
|
|
98
|
+
The approach to decoding, as at v3.7.0, is as follows.
|
|
99
|
+
|
|
100
|
+
- As audio arrives
|
|
101
|
+
- FFT to a spectrum 4 times per symbol duration & assemble a time-frequency grid with 2 bins per tone.
|
|
102
|
+
- Collect all audio samples for later full cycle FFT
|
|
103
|
+
- Search the time frequency grid for candidates based on Costas array sync score (using middle Costas block)
|
|
104
|
+
- Send candidates for decode using symbol & tone points drawn directly from the search grid to get early decodes using limited LDPC cycles.
|
|
105
|
+
|
|
106
|
+
This often decodes around three quarters of the candidates decoded by WSJT-x, with the steps below getting most of the rest:
|
|
107
|
+
|
|
108
|
+
- For candidates that haven't decoded:
|
|
109
|
+
- FFT the full cycle audio, downsample to 200 samples per second & shift the signal to f = 0
|
|
110
|
+
- Explore fine time and frequency offsets to maximise the Costas sync score
|
|
111
|
+
- Check for bad candidates (count maxima of Costas tones)
|
|
112
|
+
- Extract the LLR & wait for decoder to pick up the candidate
|
|
113
|
+
- For candidates awaiting decode from above:
|
|
114
|
+
- Try more extensive LDPC
|
|
115
|
+
- Try several 'A Priori' masks and input to more LDPC attempts, recording the output LLR on failures
|
|
116
|
+
- Try Ordered Statistics Decoding on the base LLR
|
|
117
|
+
- Try Ordered Statistics Decoding on the LLRs output from the AP+LDPC runs above
|
|
118
|
+
|
|
97
119
|
## Limitations
|
|
98
120
|
PyFT8 doesn't decode / encode *all* message types. The table below shows which are handled.
|
|
99
121
|
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|