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.
Files changed (27) hide show
  1. {pyft8-2.2.0 → pyft8-2.3.0}/PKG-INFO +5 -3
  2. {pyft8-2.2.0 → pyft8-2.3.0}/PyFT8/gui.py +8 -8
  3. pyft8-2.3.0/PyFT8/pskr_upload.py +74 -0
  4. {pyft8-2.2.0 → pyft8-2.3.0}/PyFT8/pyft8.py +27 -12
  5. {pyft8-2.2.0 → pyft8-2.3.0}/PyFT8/receiver.py +46 -29
  6. {pyft8-2.2.0 → pyft8-2.3.0}/PyFT8.egg-info/PKG-INFO +5 -3
  7. {pyft8-2.2.0 → pyft8-2.3.0}/PyFT8.egg-info/SOURCES.txt +1 -0
  8. {pyft8-2.2.0 → pyft8-2.3.0}/README.md +4 -2
  9. {pyft8-2.2.0 → pyft8-2.3.0}/pyproject.toml +1 -1
  10. pyft8-2.3.0/tests/spare.py +18 -0
  11. pyft8-2.2.0/tests/spare.py +0 -18
  12. {pyft8-2.2.0 → pyft8-2.3.0}/LICENSE +0 -0
  13. {pyft8-2.2.0 → pyft8-2.3.0}/MANIFEST.in +0 -0
  14. {pyft8-2.2.0 → pyft8-2.3.0}/PyFT8/__init__.py +0 -0
  15. {pyft8-2.2.0 → pyft8-2.3.0}/PyFT8/rigctrl.py +0 -0
  16. {pyft8-2.2.0 → pyft8-2.3.0}/PyFT8/time_utils.py +0 -0
  17. {pyft8-2.2.0 → pyft8-2.3.0}/PyFT8/transmitter.py +0 -0
  18. {pyft8-2.2.0 → pyft8-2.3.0}/PyFT8.egg-info/dependency_links.txt +0 -0
  19. {pyft8-2.2.0 → pyft8-2.3.0}/PyFT8.egg-info/entry_points.txt +0 -0
  20. {pyft8-2.2.0 → pyft8-2.3.0}/PyFT8.egg-info/requires.txt +0 -0
  21. {pyft8-2.2.0 → pyft8-2.3.0}/PyFT8.egg-info/top_level.txt +0 -0
  22. {pyft8-2.2.0 → pyft8-2.3.0}/setup.cfg +0 -0
  23. {pyft8-2.2.0 → pyft8-2.3.0}/tests/dev/osd.py +0 -0
  24. {pyft8-2.2.0 → pyft8-2.3.0}/tests/dev/test_generate_wav.py +0 -0
  25. {pyft8-2.2.0 → pyft8-2.3.0}/tests/dev/test_loopback_performance.py +0 -0
  26. {pyft8-2.2.0 → pyft8-2.3.0}/tests/plot_baseline.py +0 -0
  27. {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.2.0
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 [![PyPI Downloads](https://static.pepy.tech/personalized-badge/pyft8?period=total&units=INTERNATIONAL_SYSTEM&left_color=BLACK&right_color=GREEN&left_text=downloads)](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. At the time of writing (3-3-26) this new version 2.0.0 establishes the prior cli functionality plus an interesting GUI, on which I intend to build a transceiver function shortly.
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/user-attachments/assets/fc84702a-4d76-475b-b7fe-489f4a09deed" />
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, buttons, styles, btns_top, btn_h, btn_w, sep_h):
105
+ def _make_buttons(self, btn_defs, styles, btns_top, btn_h, btn_w, sep_h):
106
106
  self.buttons = []
107
- for i, btn in enumerate(buttons):
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[btn['style']]
110
- btn_widg = Button(btn_axs, btn['label'], color=style['fc'], hovercolor='skyblue')
111
- btn_widg.data = btn['data']
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['f']}
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, 'f':0}
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
- btn_text, btn_data = btn_widg.label.get_text(), btn_widg.data
252
- if btn_text == "CQ":
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 btn_text == "Repeat last":
263
+ if btn_action == "RPT_LAST":
256
264
  qso.set_tx_message(qso.last_tx)
257
- if btn_text == "Tx off":
265
+ if btn_action == "TX_OFF":
258
266
  console_print("[PyFT8] Set PTT Off")
259
267
  rig.ptt_off()
260
- if('m' in btn_text):
261
- qso.band_info = {'b':btn_text, 'f':btn_data}
262
- rig.set_freq_Hz(int(1000000*float(qso.band_info['f'])))
263
- console_print(f"[PyFT8] Set band: {qso.band_info['b']} {qso.band_info['f']}")
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
- from string import ascii_uppercase as ltrs, digits as digs
37
- CALL_FIELDS = [ (' ' + digs + ltrs, 36*10*27**3), (digs + ltrs, 10*27**3), (digs + ' ' * 17, 27**3),
38
- (' ' + ltrs, 27**2), (' ' + ltrs, 27), (' ' + ltrs, 1) ]
39
- CALL_TOKENS = ("DE", "QRZ", "CQ")
40
- NCALL_TOKENS_PLUS_MAX22 = 2_063_592 + 4_194_304
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 get_fields(bits, fmt):
45
- out = {}
46
- for name, n in fmt:
47
- mask = (1 << n) - 1
48
- out[name] = bits & mask
49
- bits >>= n
50
- return out
51
-
52
- def unpack(bits77):
53
- fields = get_fields(bits77, FT8_MSG_FORMAT)
54
- return (decode_call(fields["callA"]), decode_call(fields["callB"]), decode_grid(fields["grid"]))
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
- if call_int < 3:
60
- return CALL_TOKENS[call_int]
61
- call_int -= NCALL_TOKENS_PLUS_MAX22
62
- if call_int == 0:
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 CALL_FIELDS:
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 GRID_RR73s[r]
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
- e = e or (' ' in mt[0].strip() and not mt[0].startswith('CQ'))
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
- h0 = 0 if self.curr_cycle == 0 else HOPS_PER_CYCLE+1
340
- return np.sum(self.audio_in.dBgrid_main[h0:self.audio_in.dBgrid_main_ptr, :], axis = 0), self.curr_cycle
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.2.0
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 [![PyPI Downloads](https://static.pepy.tech/personalized-badge/pyft8?period=total&units=INTERNATIONAL_SYSTEM&left_color=BLACK&right_color=GREEN&left_text=downloads)](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. At the time of writing (3-3-26) this new version 2.0.0 establishes the prior cli functionality plus an interesting GUI, on which I intend to build a transceiver function shortly.
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/user-attachments/assets/fc84702a-4d76-475b-b7fe-489f4a09deed" />
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
@@ -4,6 +4,7 @@ README.md
4
4
  pyproject.toml
5
5
  PyFT8/__init__.py
6
6
  PyFT8/gui.py
7
+ PyFT8/pskr_upload.py
7
8
  PyFT8/pyft8.py
8
9
  PyFT8/receiver.py
9
10
  PyFT8/rigctrl.py
@@ -1,10 +1,12 @@
1
1
  # PyFT8 [![PyPI Downloads](https://static.pepy.tech/personalized-badge/pyft8?period=total&units=INTERNATIONAL_SYSTEM&left_color=BLACK&right_color=GREEN&left_text=downloads)](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. At the time of writing (3-3-26) this new version 2.0.0 establishes the prior cli functionality plus an interesting GUI, on which I intend to build a transceiver function shortly.
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/user-attachments/assets/fc84702a-4d76-475b-b7fe-489f4a09deed" />
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
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "PyFT8"
3
- version = "2.2.0"
3
+ version = "2.3.0"
4
4
  license = "GPL-3.0-or-later"
5
5
 
6
6
  authors = [
@@ -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()
@@ -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