PyFT8 2.2.0__tar.gz → 2.3.0__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.2.0 → pyft8-2.3.0}/PKG-INFO +5 -3
- {pyft8-2.2.0 → pyft8-2.3.0}/PyFT8/gui.py +8 -8
- pyft8-2.3.0/PyFT8/pskr_upload.py +74 -0
- {pyft8-2.2.0 → pyft8-2.3.0}/PyFT8/pyft8.py +27 -12
- {pyft8-2.2.0 → pyft8-2.3.0}/PyFT8/receiver.py +46 -29
- {pyft8-2.2.0 → pyft8-2.3.0}/PyFT8.egg-info/PKG-INFO +5 -3
- {pyft8-2.2.0 → pyft8-2.3.0}/PyFT8.egg-info/SOURCES.txt +1 -0
- {pyft8-2.2.0 → pyft8-2.3.0}/README.md +4 -2
- {pyft8-2.2.0 → pyft8-2.3.0}/pyproject.toml +1 -1
- pyft8-2.3.0/tests/spare.py +18 -0
- pyft8-2.2.0/tests/spare.py +0 -18
- {pyft8-2.2.0 → pyft8-2.3.0}/LICENSE +0 -0
- {pyft8-2.2.0 → pyft8-2.3.0}/MANIFEST.in +0 -0
- {pyft8-2.2.0 → pyft8-2.3.0}/PyFT8/__init__.py +0 -0
- {pyft8-2.2.0 → pyft8-2.3.0}/PyFT8/rigctrl.py +0 -0
- {pyft8-2.2.0 → pyft8-2.3.0}/PyFT8/time_utils.py +0 -0
- {pyft8-2.2.0 → pyft8-2.3.0}/PyFT8/transmitter.py +0 -0
- {pyft8-2.2.0 → pyft8-2.3.0}/PyFT8.egg-info/dependency_links.txt +0 -0
- {pyft8-2.2.0 → pyft8-2.3.0}/PyFT8.egg-info/entry_points.txt +0 -0
- {pyft8-2.2.0 → pyft8-2.3.0}/PyFT8.egg-info/requires.txt +0 -0
- {pyft8-2.2.0 → pyft8-2.3.0}/PyFT8.egg-info/top_level.txt +0 -0
- {pyft8-2.2.0 → pyft8-2.3.0}/setup.cfg +0 -0
- {pyft8-2.2.0 → pyft8-2.3.0}/tests/dev/osd.py +0 -0
- {pyft8-2.2.0 → pyft8-2.3.0}/tests/dev/test_generate_wav.py +0 -0
- {pyft8-2.2.0 → pyft8-2.3.0}/tests/dev/test_loopback_performance.py +0 -0
- {pyft8-2.2.0 → pyft8-2.3.0}/tests/plot_baseline.py +0 -0
- {pyft8-2.2.0 → pyft8-2.3.0}/tests/test_batch_and_live.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: PyFT8
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.3.0
|
|
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
|
|
@@ -20,10 +20,12 @@ Dynamic: license-file
|
|
|
20
20
|
# PyFT8 [](https://pepy.tech/projects/pyft8)
|
|
21
21
|
# All-Python FT8 Transceiver(WIP) GUI / Command Line Modem
|
|
22
22
|
|
|
23
|
-
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 about 70% of that achieved by WSJT-x in NORM mode, but (tbc) slightly above ft8_lib.
|
|
23
|
+
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 about 70% of that achieved by WSJT-x in NORM mode, but (tbc) slightly above ft8_lib.
|
|
24
24
|
|
|
25
25
|
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. However, please see [Rig control](https://github.com/G1OJS/PyFT8/blob/main/README.md#rig-control) below.
|
|
26
26
|
|
|
27
|
+
If you're interested in how this works, maybe have a look at [MiniPyFT8](https://github.com/G1OJS/MiniPyFT8) which puts all of the receive code in a single 300 line Python file.
|
|
28
|
+
|
|
27
29
|
## Features
|
|
28
30
|
- Doesn't try to do everything, so launches quickly (~2 seconds on my old Dell Optiplex 790)
|
|
29
31
|
- Use with or without gui (receive and send messages via command line commands)
|
|
@@ -72,7 +74,7 @@ Alternatively, you can run PyFT8 without rig control; if there is no rig found,
|
|
|
72
74
|
|
|
73
75
|
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.
|
|
74
76
|
|
|
75
|
-
<img width="640" height="480" alt="performance snapshot" src="https://github.com/
|
|
77
|
+
<img width="640" height="480" alt="performance snapshot" src="https://github.com/G1OJS/PyFT8/blob/main/performance%20snapshot.png" />
|
|
76
78
|
|
|
77
79
|
|
|
78
80
|
## Limitations
|
|
@@ -95,20 +95,20 @@ class Gui:
|
|
|
95
95
|
|
|
96
96
|
if config is not None:
|
|
97
97
|
styles = {'ctrl':{'fc':'grey','c':'black'}, 'band':{'fc':'green','c':'white'}}
|
|
98
|
-
button_defs = [{'label':'CQ','style':'ctrl','data':None}, {'label':'Repeat last','style':'ctrl','data':None},
|
|
99
|
-
{'label':'Tx off','style':'ctrl','data':None}]
|
|
98
|
+
button_defs = [{'label':'CQ','style':'ctrl','action':'CQ', 'data':None}, {'label':'Repeat last','style':'ctrl','action':'RPT_LAST','data':None},
|
|
99
|
+
{'label':'Tx off','style':'ctrl','action':'TX_OFF', 'data':None}]
|
|
100
100
|
#{'label':'Averaging','style':'ctrl','data':None}]
|
|
101
101
|
for band, freq in config['bands'].items():
|
|
102
|
-
button_defs.append({'label':band,'style':'band','data':freq})
|
|
102
|
+
button_defs.append({'label':band,'style':'band','action':'SET_FREQ','data':freq})
|
|
103
103
|
self._make_buttons(button_defs, styles, wf_top, 0.02, 0.1, 0.002)
|
|
104
104
|
|
|
105
|
-
def _make_buttons(self,
|
|
105
|
+
def _make_buttons(self, btn_defs, styles, btns_top, btn_h, btn_w, sep_h):
|
|
106
106
|
self.buttons = []
|
|
107
|
-
for i,
|
|
107
|
+
for i, btn_def in enumerate(btn_defs):
|
|
108
108
|
btn_axs = plt.axes([self.pmarg, btns_top - (i+1) * btn_h, btn_w, btn_h-sep_h])
|
|
109
|
-
style = styles[
|
|
110
|
-
btn_widg = Button(btn_axs,
|
|
111
|
-
btn_widg.
|
|
109
|
+
style = styles[btn_def['style']]
|
|
110
|
+
btn_widg = Button(btn_axs, btn_def['label'], color=style['fc'], hovercolor='skyblue')
|
|
111
|
+
btn_widg.user_data = btn_def
|
|
112
112
|
btn_widg.on_clicked(lambda event, btn_widg=btn_widg: self.on_control_click(btn_widg))
|
|
113
113
|
self.buttons.append(btn_widg)
|
|
114
114
|
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import socket
|
|
2
|
+
import struct
|
|
3
|
+
import time
|
|
4
|
+
import threading
|
|
5
|
+
import random
|
|
6
|
+
|
|
7
|
+
MAX_REPORTS = 90
|
|
8
|
+
|
|
9
|
+
class PSKR_upload:
|
|
10
|
+
# https://pskreporter.info/pskdev.html
|
|
11
|
+
# https://pskreporter.info/cgi-bin/psk-analysis.pl
|
|
12
|
+
def __init__(self, mycall, mygrid, software, tt, console_print):
|
|
13
|
+
self.RxInfoRecDescriptor_CallLocSoft = b"\x00\x03\x00\x24\x99\x92\x00\x03\x00\x01\x80\x02\xFF\xFF\x00\x00\x76\x8F\x80\x04\xFF\xFF\x00\x00\x76\x8F\x80\x08\xFF\xFF\x00\x00\x76\x8F\x00\x00"
|
|
14
|
+
self.SenderInfoRecDescriptor_CallFreqSourceStart = b"\x00\x02\x00\x2C\x99\x93\x00\x05\x80\x01\xFF\xFF\x00\x00\x76\x8F\x80\x05\x00\x04\x00\x00\x76\x8F\x80\x0A\xFF\xFF\x00\x00\x76\x8F\x80\x0B\x00\x01\x00\x00\x76\x8F\x00\x96\x00\x04"
|
|
15
|
+
self.SenderInfoRecDescriptor_SenderFreqSNRiMDModeSourceTime = b"\x00\x02\x00\x3C\x99\x93\x00\x07\x80\x01\xFF\xFF\x00\x00\x76\x8F\x80\x05\x00\x04\x00\x00\x76\x8F\x80\x06\x00\x01\x00\x00\x76\x8F\x80\x07\x00\x01\x00\x00\x76\x8F\x80\x0A\xFF\xFF\x00\x00\x76\x8F\x80\x0B\x00\x01\x00\x00\x76\x8F\x00\x96\x00\x04"
|
|
16
|
+
self.tt = tt
|
|
17
|
+
self.includeDescriptors = 0
|
|
18
|
+
self.last_report_time = time.time() - 300 + 60
|
|
19
|
+
self.addr = ("report.pskreporter.info", 4739)
|
|
20
|
+
self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
|
21
|
+
self.session_id = random.getrandbits(32)
|
|
22
|
+
self.seq = 1
|
|
23
|
+
self.reports, self.dxcalls = [], []
|
|
24
|
+
rx = self._enc_str(mycall) + self._enc_str(mygrid) + self._enc_str(software)
|
|
25
|
+
self.rx_block = self._block(b"\x99\x92", rx)
|
|
26
|
+
self.console_print = console_print
|
|
27
|
+
|
|
28
|
+
def _enc_str(self, s):
|
|
29
|
+
b = s.encode("ascii")
|
|
30
|
+
return struct.pack("B", len(b)) + b
|
|
31
|
+
|
|
32
|
+
def _block(self, block_type, payload):
|
|
33
|
+
len_with_header = len(payload) + 4
|
|
34
|
+
pad_len = (4 - (len_with_header % 4)) % 4
|
|
35
|
+
len_with_pad = len_with_header + pad_len
|
|
36
|
+
blk = block_type + struct.pack("!H", len_with_pad) + payload + b"\x00" * pad_len
|
|
37
|
+
return blk
|
|
38
|
+
|
|
39
|
+
# need to modify this to keep only the latest report for each dxcall
|
|
40
|
+
# also to check if packet is full and if so send
|
|
41
|
+
def add_report(self, dxcall, freq_hz, snr, mode, source, tt):
|
|
42
|
+
if not dxcall in self.dxcalls:
|
|
43
|
+
report = (dxcall, freq_hz, snr, mode, source, (tt // 15) * 15)
|
|
44
|
+
self.reports.append(report)
|
|
45
|
+
self.dxcalls.append(dxcall)
|
|
46
|
+
n_reports = len(self.reports)
|
|
47
|
+
if n_reports >= MAX_REPORTS or (time.time() - self.last_report_time) > 300:
|
|
48
|
+
self.send(includeDescriptors = (time.time() - self.includeDescriptors) > 3600)
|
|
49
|
+
self.includeDescriptors = time.time()
|
|
50
|
+
self.last_report_time = time.time()
|
|
51
|
+
#self.console_print(f"[pskr_upload] Added report {report} (now {len(self.reports)} reports)")
|
|
52
|
+
|
|
53
|
+
def send(self, includeDescriptors = False):
|
|
54
|
+
if not self.reports:
|
|
55
|
+
return
|
|
56
|
+
ipfx_header = struct.pack("!H", 10) + b"\x00\x00" + struct.pack("!I", self.tt) + struct.pack("!I", self.seq) + struct.pack("!I", self.session_id)
|
|
57
|
+
header = ipfx_header
|
|
58
|
+
if includeDescriptors:
|
|
59
|
+
header = header + self.RxInfoRecDescriptor_CallLocSoft + self.SenderInfoRecDescriptor_SenderFreqSNRiMDModeSourceTime
|
|
60
|
+
senders = bytearray()
|
|
61
|
+
for dxcall, freq_hz, snr, mode, source, tt in self.reports:
|
|
62
|
+
sender = self._enc_str(dxcall) + struct.pack("!I", int(freq_hz)) + struct.pack("b", int(snr)) + struct.pack("b", 0) + self._enc_str(mode) + struct.pack("B", source) + struct.pack("!I", tt)
|
|
63
|
+
senders += sender
|
|
64
|
+
packet = bytearray(header + self.rx_block + self._block(b"\x99\x93", senders))
|
|
65
|
+
struct.pack_into("!H", packet, 2, len(packet))
|
|
66
|
+
self.seq += len(self.reports)
|
|
67
|
+
self.sock.sendto(packet, self.addr)
|
|
68
|
+
self.console_print(f"[pskr_upload] Sent packet with {len(self.reports)} reports")
|
|
69
|
+
self.reports, self.dxcalls = [], []
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
#pskr = PSKReporter('G1OJS', 'IO90ju', software = 'PyFT8', tt = int(time.time()))
|
|
73
|
+
#pskr.add_report('G1OJS', 14074000, -5, 'FT8', 2, int(time.time()))
|
|
74
|
+
#pskr.send(includeDescriptors = True)
|
|
@@ -5,6 +5,7 @@ import threading
|
|
|
5
5
|
import pickle
|
|
6
6
|
import numpy as np
|
|
7
7
|
from PyFT8.receiver import Receiver, AudioIn
|
|
8
|
+
from PyFT8.pskr_upload import PSKR_upload
|
|
8
9
|
from PyFT8.gui import Gui
|
|
9
10
|
from PyFT8.transmitter import AudioOut
|
|
10
11
|
from PyFT8.time_utils import global_time_utils
|
|
@@ -12,7 +13,7 @@ from PyFT8.rigctrl import Rig
|
|
|
12
13
|
|
|
13
14
|
MAX_TX_START_SECONDS = 2.5
|
|
14
15
|
T_CYC = 15
|
|
15
|
-
rig, gui, qso, worked_before = None, None, None, None
|
|
16
|
+
rig, gui, qso, worked_before, pskr_upload = None, None, None, None, None
|
|
16
17
|
|
|
17
18
|
def get_config(config_folder):
|
|
18
19
|
import configparser
|
|
@@ -25,6 +26,7 @@ def get_config(config_folder):
|
|
|
25
26
|
config['rig'] = {'port': 'COM4', 'baud_rate':9600,
|
|
26
27
|
'set_freq_command':'FEFE88E0.05.0000000000.FD', 'set_freq_value':'5|5|vfBcdLU|1|0',
|
|
27
28
|
'ptt_on_command':'FEFE88E0.1C00.01.FD', 'ptt_off_command':'FEFE88E0.1C00.00.FD'}
|
|
29
|
+
config['pskreporter'] = {'upload':'N'}
|
|
28
30
|
with open(ini_file, 'w') as f:
|
|
29
31
|
config.write(f)
|
|
30
32
|
console_print(f"Wrote default config to {ini_file}")
|
|
@@ -83,7 +85,7 @@ class Logging:
|
|
|
83
85
|
'rst_sent':rpts['sent'], 'rst_rcvd':rpts['rcvd'],
|
|
84
86
|
'qso_date':time.strftime("%Y%m%d", times['time_on']), 'qso_date_off':time.strftime("%Y%m%d", times['time_off']),
|
|
85
87
|
'time_on':time.strftime("%H%M%S", times['time_on']), 'time_off':time.strftime("%H%M%S", times['time_on']),
|
|
86
|
-
'band':band_info['b'], 'freq':band_info['
|
|
88
|
+
'band':band_info['b'], 'freq':band_info['fMHz']}
|
|
87
89
|
with open(self.adif_log_file,'a') as f:
|
|
88
90
|
f.write(f"\n")
|
|
89
91
|
for k, v in log_dict.items():
|
|
@@ -121,7 +123,7 @@ class FT8_QSO:
|
|
|
121
123
|
self.logging = logging
|
|
122
124
|
if config is not None:
|
|
123
125
|
self.mStation = {'c':config['station']['call'], 'g':config['station']['grid']}
|
|
124
|
-
self.band_info = {'b':None, '
|
|
126
|
+
self.band_info = {'b':None, 'fMHz':0}
|
|
125
127
|
self.tx_freq = 750
|
|
126
128
|
threading.Thread(target = self._transmitter, daemon = True).start()
|
|
127
129
|
self.clear()
|
|
@@ -156,6 +158,7 @@ class FT8_QSO:
|
|
|
156
158
|
if self.tx_cycle is None:
|
|
157
159
|
self.tx_cycle = global_time_utils.curr_cycle_from_time()
|
|
158
160
|
self.tx_freq = clear_frequencies[self.tx_cycle]
|
|
161
|
+
console_print(f"[PyFT8] Set tx cycle = {self.tx_cycle} f = {self.tx_freq}")
|
|
159
162
|
console_print(f"Transmitting {self.message_to_transmit} on cycle {self.tx_cycle}")
|
|
160
163
|
symbols = audio_out.create_ft8_symbols(self.message_to_transmit)
|
|
161
164
|
audio_data = audio_out.create_ft8_wave(symbols, f_base = self.tx_freq)
|
|
@@ -236,6 +239,10 @@ def on_decode(c):
|
|
|
236
239
|
message = Message(c)
|
|
237
240
|
if gui:
|
|
238
241
|
gui.add_message_box(message)
|
|
242
|
+
if qso.band_info['b'] is not None and pskr_upload is not None:
|
|
243
|
+
dx_call = c.msg_tuple[1]
|
|
244
|
+
if dx_call != 'not':
|
|
245
|
+
pskr_upload.add_report(dx_call, int(1000000*float(qso.band_info['fMHz'])) + c.fHz, c.snr, 'FT8', 2, int(time.time()))
|
|
239
246
|
print(message.wsjtx_screen_format())
|
|
240
247
|
|
|
241
248
|
def on_busy_profile(busy_profile, cycle):
|
|
@@ -248,19 +255,22 @@ def on_busy_profile(busy_profile, cycle):
|
|
|
248
255
|
console_print(f"[on_busy] Set Tx freq to {clear_frequencies[cycle]:6.1f} for cycle {cycle}")
|
|
249
256
|
|
|
250
257
|
def on_control_click(btn_widg):
|
|
251
|
-
|
|
252
|
-
|
|
258
|
+
btn_def = btn_widg.user_data
|
|
259
|
+
btn_action = btn_def['action']
|
|
260
|
+
if btn_action == "CQ":
|
|
253
261
|
mc, mg = config['station']['call'], config['station']['grid']
|
|
254
262
|
qso.set_tx_message(f"CQ {mc} {mg}")
|
|
255
|
-
if
|
|
263
|
+
if btn_action == "RPT_LAST":
|
|
256
264
|
qso.set_tx_message(qso.last_tx)
|
|
257
|
-
if
|
|
265
|
+
if btn_action == "TX_OFF":
|
|
258
266
|
console_print("[PyFT8] Set PTT Off")
|
|
259
267
|
rig.ptt_off()
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
268
|
+
qso.tx_cycle = None
|
|
269
|
+
if(btn_action == 'SET_FREQ'):
|
|
270
|
+
btn_text, freqMHz = btn_widg.label.get_text(), btn_def['data']
|
|
271
|
+
qso.band_info = {'b':btn_text, 'fMHz':freqMHz}
|
|
272
|
+
rig.set_freq_Hz(int(1000000*float(qso.band_info['fMHz'])))
|
|
273
|
+
console_print(f"[PyFT8] Set band: {qso.band_info['b']} {qso.band_info['fMHz']}")
|
|
264
274
|
|
|
265
275
|
def on_msg_click(message):
|
|
266
276
|
progress_qso(message)
|
|
@@ -273,7 +283,7 @@ def console_print(text, color = 'white'):
|
|
|
273
283
|
print(text)
|
|
274
284
|
|
|
275
285
|
def cli():
|
|
276
|
-
global audio_in, audio_out, output_device_idx, rig, gui, qso, config, clear_frequencies
|
|
286
|
+
global audio_in, audio_out, output_device_idx, rig, gui, qso, config, clear_frequencies, pskr_upload
|
|
277
287
|
import time
|
|
278
288
|
parser = argparse.ArgumentParser(prog='PyFT8rx', description = 'Command Line FT8 decoder')
|
|
279
289
|
parser.add_argument('-c', '--config_folder', help = 'Location of config folder e.g. C:/Users/drala/Documents/Projects/GitHub/G1OJS/PyFT8_cfg', default = './')
|
|
@@ -289,6 +299,11 @@ def cli():
|
|
|
289
299
|
config_folder = f"{args.config_folder}".strip()
|
|
290
300
|
get_config(config_folder)
|
|
291
301
|
logging = Logging(config_folder)
|
|
302
|
+
mc, mg = config['station']['call'], config['station']['grid']
|
|
303
|
+
if mc is not None and 'pskreporter' in config.keys():
|
|
304
|
+
if config['pskreporter']['upload'] == 'Y':
|
|
305
|
+
pskr_upload = PSKR_upload(mc, mg, software = 'PyFT8 v2.3.0', tt = int(time.time()), console_print = console_print) if not mc is None else None
|
|
306
|
+
console_print(f"[PyFT8] Spots will upload to pskreporter")
|
|
292
307
|
qso = FT8_QSO(logging)
|
|
293
308
|
rig = Rig(config)
|
|
294
309
|
|
|
@@ -33,36 +33,48 @@ global_time_utils.set_cycle_length(T_CYC)
|
|
|
33
33
|
|
|
34
34
|
|
|
35
35
|
#=========== Unpacking functions ========================================
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
GRID_RR73s = ('', '', 'RRR', 'RR73', '73')
|
|
42
|
-
FT8_MSG_FORMAT = (("i3", 3), ("grid", 16), ("callB",29), ("callA",29))
|
|
36
|
+
def get_bits(bits, n):
|
|
37
|
+
mask = (1 << n) - 1
|
|
38
|
+
out = bits & mask
|
|
39
|
+
bits >>= n
|
|
40
|
+
return out, bits
|
|
43
41
|
|
|
44
|
-
def
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
42
|
+
def unpack(bits):
|
|
43
|
+
i3, bits = get_bits(bits,3)
|
|
44
|
+
if i3 == 0:
|
|
45
|
+
n3, bits = get_bits(bits,3)
|
|
46
|
+
if n3 == 0:
|
|
47
|
+
return ('Free text','not','implemented')
|
|
48
|
+
else:
|
|
49
|
+
return (['DXpedition','Field Day', 'Field Day', 'Telemetry'][n3-1],'not','implemented')
|
|
50
|
+
elif i3 == 1:
|
|
51
|
+
gr, bits = get_bits(bits,16)
|
|
52
|
+
cb, bits = get_bits(bits,29)
|
|
53
|
+
ca, bits = get_bits(bits,29)
|
|
54
|
+
return (decode_call(ca), decode_call(cb), decode_grid(gr))
|
|
55
|
+
elif i3 == 2:
|
|
56
|
+
return ('EU VHF','not','implemented')
|
|
57
|
+
elif i3 == 3:
|
|
58
|
+
return ('RTTY RU','not','implemented')
|
|
59
|
+
elif i3 == 4:
|
|
60
|
+
return ('Nonstd Call','not','implemented')
|
|
61
|
+
elif i3 == 5:
|
|
62
|
+
return ('EU VHF','not','implemented')
|
|
55
63
|
|
|
56
64
|
def decode_call(call_int):
|
|
65
|
+
from string import ascii_uppercase as ltrs, digits as digs
|
|
66
|
+
table_7 = {'DE':(0,0),'QRZ':(1,1),'CQ':(2,2), 'CQ nnn':(3,1002),'CQ x':(1004,1029),
|
|
67
|
+
'CQ xx':(1031,1731),'CQ xxxx':(21443,532443),'<....>':(2063592,2063592+4194303)}
|
|
68
|
+
call_fields = [ (' ' + digs + ltrs, 36*10*27**3), (digs + ltrs, 10*27**3), (digs + ' ' * 17, 27**3),
|
|
69
|
+
(' ' + ltrs, 27**2), (' ' + ltrs, 27), (' ' + ltrs, 1) ]
|
|
57
70
|
portable = call_int & 1
|
|
58
71
|
call_int >>= 1
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
return '<...>'
|
|
72
|
+
for ct, (lo, hi) in table_7.items():
|
|
73
|
+
if lo <= call_int <= hi:
|
|
74
|
+
return ct
|
|
75
|
+
call_int -= (2063592 + 4194304)
|
|
64
76
|
chars = []
|
|
65
|
-
for alphabet, div in
|
|
77
|
+
for alphabet, div in call_fields:
|
|
66
78
|
idx, call_int = divmod(call_int, div)
|
|
67
79
|
chars.append(alphabet[idx])
|
|
68
80
|
call = ''.join(chars).strip()
|
|
@@ -77,7 +89,7 @@ def decode_grid(grid_int):
|
|
|
77
89
|
return chr(65+a) + chr(65+b) + str(c) + str(d)
|
|
78
90
|
r = g15 - 32400
|
|
79
91
|
if r <= 4:
|
|
80
|
-
return
|
|
92
|
+
return ('', '', 'RRR', 'RR73', '73')[r]
|
|
81
93
|
snr = r - 35
|
|
82
94
|
ir = grid_int >> 15
|
|
83
95
|
prefix = 'R' if ir else ''
|
|
@@ -279,9 +291,10 @@ class Candidate:
|
|
|
279
291
|
self.decode_completed = time.time()
|
|
280
292
|
|
|
281
293
|
def validate(self, msg_tuple):
|
|
282
|
-
mt = msg_tuple
|
|
283
294
|
e = False
|
|
284
|
-
|
|
295
|
+
# checking if this is needed after adding full table_7 info and branches on i3, n3
|
|
296
|
+
#mt = msg_tuple
|
|
297
|
+
#e = e or (' ' in mt[0].strip() and not mt[0].startswith('CQ'))
|
|
285
298
|
#e = e or (' ' in mt[1].strip())
|
|
286
299
|
if not e:
|
|
287
300
|
return ' '.join(self.msg_tuple)
|
|
@@ -336,8 +349,12 @@ class Receiver():
|
|
|
336
349
|
return cands
|
|
337
350
|
|
|
338
351
|
def get_busy_profile(self):
|
|
339
|
-
|
|
340
|
-
|
|
352
|
+
from numpy.lib.stride_tricks import sliding_window_view
|
|
353
|
+
h0 = 0 if self.curr_cycle == 0 else HOPS_PER_CYCLE+1
|
|
354
|
+
fbin_sum = np.sum(self.audio_in.dBgrid_main[h0:self.audio_in.dBgrid_main_ptr, :], axis = 0)
|
|
355
|
+
windows = sliding_window_view(fbin_sum, 8*BPT)
|
|
356
|
+
bp = windows.max(axis=1)
|
|
357
|
+
return bp, self.curr_cycle
|
|
341
358
|
|
|
342
359
|
def manage_cycle(self):
|
|
343
360
|
dashes = "======================================================"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: PyFT8
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.3.0
|
|
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
|
|
@@ -20,10 +20,12 @@ Dynamic: license-file
|
|
|
20
20
|
# PyFT8 [](https://pepy.tech/projects/pyft8)
|
|
21
21
|
# All-Python FT8 Transceiver(WIP) GUI / Command Line Modem
|
|
22
22
|
|
|
23
|
-
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 about 70% of that achieved by WSJT-x in NORM mode, but (tbc) slightly above ft8_lib.
|
|
23
|
+
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 about 70% of that achieved by WSJT-x in NORM mode, but (tbc) slightly above ft8_lib.
|
|
24
24
|
|
|
25
25
|
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. However, please see [Rig control](https://github.com/G1OJS/PyFT8/blob/main/README.md#rig-control) below.
|
|
26
26
|
|
|
27
|
+
If you're interested in how this works, maybe have a look at [MiniPyFT8](https://github.com/G1OJS/MiniPyFT8) which puts all of the receive code in a single 300 line Python file.
|
|
28
|
+
|
|
27
29
|
## Features
|
|
28
30
|
- Doesn't try to do everything, so launches quickly (~2 seconds on my old Dell Optiplex 790)
|
|
29
31
|
- Use with or without gui (receive and send messages via command line commands)
|
|
@@ -72,7 +74,7 @@ Alternatively, you can run PyFT8 without rig control; if there is no rig found,
|
|
|
72
74
|
|
|
73
75
|
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.
|
|
74
76
|
|
|
75
|
-
<img width="640" height="480" alt="performance snapshot" src="https://github.com/
|
|
77
|
+
<img width="640" height="480" alt="performance snapshot" src="https://github.com/G1OJS/PyFT8/blob/main/performance%20snapshot.png" />
|
|
76
78
|
|
|
77
79
|
|
|
78
80
|
## Limitations
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
# PyFT8 [](https://pepy.tech/projects/pyft8)
|
|
2
2
|
# All-Python FT8 Transceiver(WIP) 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 about 70% of that achieved by WSJT-x in NORM mode, but (tbc) slightly above ft8_lib.
|
|
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 about 70% of that achieved by WSJT-x in NORM mode, but (tbc) slightly above ft8_lib.
|
|
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. However, please see [Rig control](https://github.com/G1OJS/PyFT8/blob/main/README.md#rig-control) below.
|
|
7
7
|
|
|
8
|
+
If you're interested in how this works, maybe have a look at [MiniPyFT8](https://github.com/G1OJS/MiniPyFT8) which puts all of the receive code in a single 300 line Python file.
|
|
9
|
+
|
|
8
10
|
## Features
|
|
9
11
|
- Doesn't try to do everything, so launches quickly (~2 seconds on my old Dell Optiplex 790)
|
|
10
12
|
- Use with or without gui (receive and send messages via command line commands)
|
|
@@ -53,7 +55,7 @@ Alternatively, you can run PyFT8 without rig control; if there is no rig found,
|
|
|
53
55
|
|
|
54
56
|
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.
|
|
55
57
|
|
|
56
|
-
<img width="640" height="480" alt="performance snapshot" src="https://github.com/
|
|
58
|
+
<img width="640" height="480" alt="performance snapshot" src="https://github.com/G1OJS/PyFT8/blob/main/performance%20snapshot.png" />
|
|
57
59
|
|
|
58
60
|
|
|
59
61
|
## Limitations
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import matplotlib.pyplot as plt
|
|
3
|
+
import pickle
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
fig, axs = plt.subplots(3,1, figsize = (8,10))
|
|
7
|
+
|
|
8
|
+
data = [0,0,0,0,0,1,2,1,3,2,5,4,3,4,5,6,7,9,9,9,1,9,8,1,9,7,3,4,3,4,2,1]
|
|
9
|
+
data = [0,0,0,0,0,9,9,9,9,9,9,9,9,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]
|
|
10
|
+
c = np.ma.convolve(data, [1,1,1,1,1,1,1,1])/8
|
|
11
|
+
c = np.roll(c, -3)
|
|
12
|
+
axs[0].plot(data)
|
|
13
|
+
axs[0].plot(c)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
plt.tight_layout()
|
|
18
|
+
plt.show()
|
pyft8-2.2.0/tests/spare.py
DELETED
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
import numpy as np
|
|
2
|
-
import matplotlib.pyplot as plt
|
|
3
|
-
import pickle
|
|
4
|
-
|
|
5
|
-
with open('loop_scores.pkl', 'rb') as f:
|
|
6
|
-
ls = pickle.load(f)
|
|
7
|
-
|
|
8
|
-
with open('vector_scores.pkl', 'rb') as f:
|
|
9
|
-
vs = pickle.load(f)
|
|
10
|
-
|
|
11
|
-
fig, axs = plt.subplots(3,1, figsize = (8,10))
|
|
12
|
-
vx = 100
|
|
13
|
-
vm = 30
|
|
14
|
-
im0 = axs[0].imshow(ls, vmax = vx, vmin = vm, aspect = 4, origin = 'lower')
|
|
15
|
-
im1 = axs[1].imshow(vs,vmax = vx, vmin = vm, aspect = 4, origin = 'lower')
|
|
16
|
-
im = axs[2].imshow(vs[:100, :980] - ls[:100, :980], aspect = 4, vmax = 40, vmin = -40, origin = 'lower')
|
|
17
|
-
plt.tight_layout()
|
|
18
|
-
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
|