PyFT8 2.3.0__tar.gz → 2.3.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.
Files changed (26) hide show
  1. {pyft8-2.3.0 → pyft8-2.3.1}/PKG-INFO +14 -4
  2. {pyft8-2.3.0 → pyft8-2.3.1}/PyFT8/pskr_upload.py +19 -17
  3. {pyft8-2.3.0 → pyft8-2.3.1}/PyFT8/pyft8.py +23 -7
  4. {pyft8-2.3.0 → pyft8-2.3.1}/PyFT8/receiver.py +7 -7
  5. {pyft8-2.3.0 → pyft8-2.3.1}/PyFT8/transmitter.py +1 -1
  6. {pyft8-2.3.0 → pyft8-2.3.1}/PyFT8.egg-info/PKG-INFO +14 -4
  7. {pyft8-2.3.0 → pyft8-2.3.1}/README.md +13 -3
  8. {pyft8-2.3.0 → pyft8-2.3.1}/pyproject.toml +1 -1
  9. {pyft8-2.3.0 → pyft8-2.3.1}/LICENSE +0 -0
  10. {pyft8-2.3.0 → pyft8-2.3.1}/MANIFEST.in +0 -0
  11. {pyft8-2.3.0 → pyft8-2.3.1}/PyFT8/__init__.py +0 -0
  12. {pyft8-2.3.0 → pyft8-2.3.1}/PyFT8/gui.py +0 -0
  13. {pyft8-2.3.0 → pyft8-2.3.1}/PyFT8/rigctrl.py +0 -0
  14. {pyft8-2.3.0 → pyft8-2.3.1}/PyFT8/time_utils.py +0 -0
  15. {pyft8-2.3.0 → pyft8-2.3.1}/PyFT8.egg-info/SOURCES.txt +0 -0
  16. {pyft8-2.3.0 → pyft8-2.3.1}/PyFT8.egg-info/dependency_links.txt +0 -0
  17. {pyft8-2.3.0 → pyft8-2.3.1}/PyFT8.egg-info/entry_points.txt +0 -0
  18. {pyft8-2.3.0 → pyft8-2.3.1}/PyFT8.egg-info/requires.txt +0 -0
  19. {pyft8-2.3.0 → pyft8-2.3.1}/PyFT8.egg-info/top_level.txt +0 -0
  20. {pyft8-2.3.0 → pyft8-2.3.1}/setup.cfg +0 -0
  21. {pyft8-2.3.0 → pyft8-2.3.1}/tests/dev/osd.py +0 -0
  22. {pyft8-2.3.0 → pyft8-2.3.1}/tests/dev/test_generate_wav.py +0 -0
  23. {pyft8-2.3.0 → pyft8-2.3.1}/tests/dev/test_loopback_performance.py +0 -0
  24. {pyft8-2.3.0 → pyft8-2.3.1}/tests/plot_baseline.py +0 -0
  25. {pyft8-2.3.0 → pyft8-2.3.1}/tests/spare.py +0 -0
  26. {pyft8-2.3.0 → pyft8-2.3.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: 2.3.0
3
+ Version: 2.3.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
@@ -18,7 +18,7 @@ Requires-Dist: pyaudio
18
18
  Dynamic: license-file
19
19
 
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
- # All-Python FT8 Transceiver(WIP) GUI / Command Line Modem
21
+ # All-Python FT8 Transceiver GUI / Command Line Modem
22
22
 
23
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
 
@@ -30,11 +30,19 @@ If you're interested in how this works, maybe have a look at [MiniPyFT8](https:/
30
30
  - Doesn't try to do everything, so launches quickly (~2 seconds on my old Dell Optiplex 790)
31
31
  - Use with or without gui (receive and send messages via command line commands)
32
32
  - GUI provides simultaneous views of odd and even cycles
33
- - GUI shows worked-before info on CQ calls (not yet on PyPi)
33
+ - GUI shows worked-before info on CQ calls
34
34
  - Messages overlaid on waterfall signals that produce them
35
35
  - Automatically chooses clearest Tx frequency
36
36
  - Modern programming language throughout
37
37
  - Finds sound cards by keywords so follows them if windows moves them ...
38
+ - Logs QSOs to ADIF file and all spots to WSJTX-style ALL.txt file
39
+ - Uploads spots to pskreporter
40
+
41
+ To enable uploading of spots to pskreporter, make sure that your .ini file includes
42
+ ```
43
+ [pskreporter]
44
+ upload = Y
45
+ ```
38
46
 
39
47
  <img width="1003" height="1020" alt="image" src="https://github.com/user-attachments/assets/bf6e3f78-531a-4c9b-ab2b-b51cc04ad980" />
40
48
 
@@ -112,6 +120,7 @@ Note - section 9 of the QEX paper states that the above two WSJT-X resources are
112
120
  **FT8 decoding explorations / explanations**
113
121
  - [VK3JPK's FT8 notes](https://github.com/vk3jpk/ft8-notes) including comprehensive [Python source code](https://github.com/vk3jpk/ft8-notes/blob/master/ft8.py)
114
122
  - [G4JNT notes on LDPC coding process](http://www.g4jnt.com/WSJT-X_LdpcModesCodingProcess.pdf)
123
+ - [Kristian Glass's 2025-05-06 Understanding the FT8 binary protocol](https://notes.doismellburning.co.uk/notebook/2025-05-06-understanding-the-ft8-binary-protocol/)
115
124
 
116
125
  **FT8 decoding in hardware**
117
126
  - [Optimizing the (Web-888) FT8 Skimmer Experience](https://www.rx-888.com/web/design/digi.html) (see also [RX-888 project](https://www.rx-888.com/) )
@@ -122,7 +131,8 @@ Note - section 9 of the QEX paper states that the above two WSJT-X resources are
122
131
  - [Post about ft8play](https://groups.io/g/FT8-Digital-Mode/topic/i_made_a_thing_ft8play/107846361)
123
132
 
124
133
  **Browser-based decoder/encoders**
125
- - [ft8js](https://e04.github.io/ft8js/example/browser/index.html) - source [github](https://github.com/e04/ft8js?tab=readme-ov-file), uses [FT8_lib](https://github.com/kgoba/ft8_lib)
134
+ - [ft8js by e04](https://e04.github.io/ft8js/example/browser/index.html) - source [github](https://github.com/e04/ft8js?tab=readme-ov-file), uses web-assembled version of [FT8_lib](https://github.com/kgoba/ft8_lib)
135
+ - [ft8ts by e04](https://github.com/e04/ft8ts), - pure TypeScript implementation
126
136
  - [ChromeFT8 Browser Extension](https://github.com/Transwarp8/ChromeFT8), decoder adapted from [ft8js](https://e04.github.io/ft8js/example/browser/index.html)
127
137
 
128
138
  <script data-goatcounter="https://g1ojs-github.goatcounter.com/count" async src="//gc.zgo.at/count.js"></script>
@@ -11,7 +11,6 @@ class PSKR_upload:
11
11
  # https://pskreporter.info/cgi-bin/psk-analysis.pl
12
12
  def __init__(self, mycall, mygrid, software, tt, console_print):
13
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
14
  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
15
  self.tt = tt
17
16
  self.includeDescriptors = 0
@@ -20,10 +19,12 @@ class PSKR_upload:
20
19
  self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
21
20
  self.session_id = random.getrandbits(32)
22
21
  self.seq = 1
23
- self.reports, self.dxcalls = [], []
22
+ self.reports = {}
24
23
  rx = self._enc_str(mycall) + self._enc_str(mygrid) + self._enc_str(software)
25
24
  self.rx_block = self._block(b"\x99\x92", rx)
26
25
  self.console_print = console_print
26
+ self.lock = threading.Lock()
27
+ threading.Thread(target = self._check_for_send, daemon = True).start()
27
28
 
28
29
  def _enc_str(self, s):
29
30
  b = s.encode("ascii")
@@ -36,21 +37,21 @@ class PSKR_upload:
36
37
  blk = block_type + struct.pack("!H", len_with_pad) + payload + b"\x00" * pad_len
37
38
  return blk
38
39
 
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
40
  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)")
41
+ with self.lock:
42
+ self.reports[dxcall] = (dxcall, freq_hz, snr, mode, source, (tt // 15) * 15)
52
43
 
53
- def send(self, includeDescriptors = False):
44
+ def _check_for_send(self):
45
+ while True:
46
+ time.sleep(60)
47
+ with self.lock:
48
+ n_reports = len(self.reports)
49
+ if n_reports >= MAX_REPORTS or (time.time() - self.last_report_time) > 300:
50
+ self._send(includeDescriptors = (time.time() - self.includeDescriptors) > 3600)
51
+ self.includeDescriptors = time.time()
52
+ self.last_report_time = time.time()
53
+
54
+ def _send(self, includeDescriptors = False):
54
55
  if not self.reports:
55
56
  return
56
57
  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)
@@ -58,7 +59,8 @@ class PSKR_upload:
58
59
  if includeDescriptors:
59
60
  header = header + self.RxInfoRecDescriptor_CallLocSoft + self.SenderInfoRecDescriptor_SenderFreqSNRiMDModeSourceTime
60
61
  senders = bytearray()
61
- for dxcall, freq_hz, snr, mode, source, tt in self.reports:
62
+ for dxcall, freq_hz, snr, mode, source, tt in self.reports.values():
63
+ print(f"Sending report {dxcall}, {freq_hz}, {snr}, {source}, {tt}")
62
64
  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
65
  senders += sender
64
66
  packet = bytearray(header + self.rx_block + self._block(b"\x99\x93", senders))
@@ -66,7 +68,7 @@ class PSKR_upload:
66
68
  self.seq += len(self.reports)
67
69
  self.sock.sendto(packet, self.addr)
68
70
  self.console_print(f"[pskr_upload] Sent packet with {len(self.reports)} reports")
69
- self.reports, self.dxcalls = [], []
71
+ self.reports = {}
70
72
 
71
73
 
72
74
  #pskr = PSKReporter('G1OJS', 'IO90ju', software = 'PyFT8', tt = int(time.time()))
@@ -11,11 +11,13 @@ from PyFT8.transmitter import AudioOut
11
11
  from PyFT8.time_utils import global_time_utils
12
12
  from PyFT8.rigctrl import Rig
13
13
 
14
+ VER = '2.3.1'
15
+
14
16
  MAX_TX_START_SECONDS = 2.5
15
17
  T_CYC = 15
16
18
  rig, gui, qso, worked_before, pskr_upload = None, None, None, None, None
17
19
 
18
- def get_config(config_folder):
20
+ def get_config():
19
21
  import configparser
20
22
  global config
21
23
  config = configparser.ConfigParser()
@@ -41,7 +43,7 @@ def parse_from_adif_rec(rec, field):
41
43
  return rec[p2+1: p2+1+n]
42
44
 
43
45
  class Logging:
44
- def __init__(self, config_folder):
46
+ def __init__(self):
45
47
  self.adif_log_file = f"{config_folder}/PyFT8.adi"
46
48
  self.worked_before_file = f"{config_folder}/PyFT8_wb.pkl"
47
49
  if(not os.path.exists(self.adif_log_file)):
@@ -117,6 +119,10 @@ class Message:
117
119
  def wsjtx_screen_format(self):
118
120
  return f"{self.cyclestart['string']} {self.snr:+03d} {self.dt:4.1f} {self.fHz:4.0f} ~ {self.msg}"
119
121
 
122
+ def wsjtx_all_txt_format(self):
123
+ fMHz = float(qso.band_info['fMHz']) if qso.band_info['fMHz'] is not None else 0
124
+ return f"{self.cyclestart['string']} {fMHz:8.3f} Rx FT8 {self.snr:+03d} {self.dt:4.1f} {self.fHz:4.0f} ~ {self.msg}"
125
+
120
126
 
121
127
  class FT8_QSO:
122
128
  def __init__(self, logging):
@@ -234,6 +240,13 @@ def wait_for_keyboard():
234
240
  except KeyboardInterrupt:
235
241
  pass
236
242
 
243
+ def write_all_txt_row(message):
244
+ all_file = f"{config_folder}/ALL.txt"
245
+ mode = 'w' if not os.path.exists(all_file) else 'a'
246
+ row = message.wsjtx_all_txt_format()
247
+ with open(all_file, mode) as f:
248
+ f.write(f"{row}\n")
249
+
237
250
  #============= Callbacks for GUI ==========================================================
238
251
  def on_decode(c):
239
252
  message = Message(c)
@@ -242,8 +255,9 @@ def on_decode(c):
242
255
  if qso.band_info['b'] is not None and pskr_upload is not None:
243
256
  dx_call = c.msg_tuple[1]
244
257
  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()))
258
+ pskr_upload.add_report(dx_call, int(1000000*float(qso.band_info['fMHz'])) + c.fHz, c.snr, 'FT8', 1, int(time.time()))
246
259
  print(message.wsjtx_screen_format())
260
+ write_all_txt_row(message)
247
261
 
248
262
  def on_busy_profile(busy_profile, cycle):
249
263
  if output_device_idx is None:
@@ -253,6 +267,8 @@ def on_busy_profile(busy_profile, cycle):
253
267
  idx = np.argmin(busy_profile[f0_idx:fn_idx])
254
268
  clear_frequencies[cycle] = (f0_idx + idx) * audio_in.df
255
269
  console_print(f"[on_busy] Set Tx freq to {clear_frequencies[cycle]:6.1f} for cycle {cycle}")
270
+ if qso.band_info['b'] is None:
271
+ console_print(f"[PyFT8] Band not set; please select a band.", color = 'red')
256
272
 
257
273
  def on_control_click(btn_widg):
258
274
  btn_def = btn_widg.user_data
@@ -283,7 +299,7 @@ def console_print(text, color = 'white'):
283
299
  print(text)
284
300
 
285
301
  def cli():
286
- global audio_in, audio_out, output_device_idx, rig, gui, qso, config, clear_frequencies, pskr_upload
302
+ global audio_in, audio_out, output_device_idx, rig, gui, qso, config, config_folder, clear_frequencies, pskr_upload
287
303
  import time
288
304
  parser = argparse.ArgumentParser(prog='PyFT8rx', description = 'Command Line FT8 decoder')
289
305
  parser.add_argument('-c', '--config_folder', help = 'Location of config folder e.g. C:/Users/drala/Documents/Projects/GitHub/G1OJS/PyFT8_cfg', default = './')
@@ -297,12 +313,12 @@ def cli():
297
313
 
298
314
  output_device_idx = None
299
315
  config_folder = f"{args.config_folder}".strip()
300
- get_config(config_folder)
301
- logging = Logging(config_folder)
316
+ get_config()
317
+ logging = Logging()
302
318
  mc, mg = config['station']['call'], config['station']['grid']
303
319
  if mc is not None and 'pskreporter' in config.keys():
304
320
  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
321
+ pskr_upload = PSKR_upload(mc, mg, software = f"PyFT8 v{VER}", tt = int(time.time()), console_print = console_print) if not mc is None else None
306
322
  console_print(f"[PyFT8] Spots will upload to pskreporter")
307
323
  qso = FT8_QSO(logging)
308
324
  rig = Rig(config)
@@ -47,13 +47,11 @@ def unpack(bits):
47
47
  return ('Free text','not','implemented')
48
48
  else:
49
49
  return (['DXpedition','Field Day', 'Field Day', 'Telemetry'][n3-1],'not','implemented')
50
- elif i3 == 1:
50
+ elif i3 == 1 or i3 == 2: # 1 = Std Msg incl /R 2 = 'EU VHF' = Std Msg incl /P
51
51
  gr, bits = get_bits(bits,16)
52
52
  cb, bits = get_bits(bits,29)
53
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')
54
+ return (decode_call(ca, i3), decode_call(cb, i3), decode_grid(gr))
57
55
  elif i3 == 3:
58
56
  return ('RTTY RU','not','implemented')
59
57
  elif i3 == 4:
@@ -61,13 +59,13 @@ def unpack(bits):
61
59
  elif i3 == 5:
62
60
  return ('EU VHF','not','implemented')
63
61
 
64
- def decode_call(call_int):
62
+ def decode_call(call_int, i3):
65
63
  from string import ascii_uppercase as ltrs, digits as digs
66
64
  table_7 = {'DE':(0,0),'QRZ':(1,1),'CQ':(2,2), 'CQ nnn':(3,1002),'CQ x':(1004,1029),
67
65
  'CQ xx':(1031,1731),'CQ xxxx':(21443,532443),'<....>':(2063592,2063592+4194303)}
68
66
  call_fields = [ (' ' + digs + ltrs, 36*10*27**3), (digs + ltrs, 10*27**3), (digs + ' ' * 17, 27**3),
69
67
  (' ' + ltrs, 27**2), (' ' + ltrs, 27), (' ' + ltrs, 1) ]
70
- portable = call_int & 1
68
+ portable_rover = call_int & 1
71
69
  call_int >>= 1
72
70
  for ct, (lo, hi) in table_7.items():
73
71
  if lo <= call_int <= hi:
@@ -78,7 +76,9 @@ def decode_call(call_int):
78
76
  idx, call_int = divmod(call_int, div)
79
77
  chars.append(alphabet[idx])
80
78
  call = ''.join(chars).strip()
81
- return call + '/P' if portable else call
79
+ if portable_rover:
80
+ call = call + ('/P' if i3 == 2 else '/R')
81
+ return call
82
82
 
83
83
  def decode_grid(grid_int):
84
84
  g15 = grid_int & 0x7FFF
@@ -67,7 +67,7 @@ def _pack_message(c1, c2, gr):
67
67
  c28a, p1a = pack_ft8_c28(c1)
68
68
  c28b, p1b = pack_ft8_c28(c2)
69
69
  g15, ir = pack_ft8_g15(gr)
70
- i3 = 1
70
+ i3 = 2 if p1a or p1b else 1
71
71
  n3 = 0
72
72
  symbols, bits77 = [], 0
73
73
  if(c28a>=0 and c28b>=0):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: PyFT8
3
- Version: 2.3.0
3
+ Version: 2.3.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
@@ -18,7 +18,7 @@ Requires-Dist: pyaudio
18
18
  Dynamic: license-file
19
19
 
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
- # All-Python FT8 Transceiver(WIP) GUI / Command Line Modem
21
+ # All-Python FT8 Transceiver GUI / Command Line Modem
22
22
 
23
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
 
@@ -30,11 +30,19 @@ If you're interested in how this works, maybe have a look at [MiniPyFT8](https:/
30
30
  - Doesn't try to do everything, so launches quickly (~2 seconds on my old Dell Optiplex 790)
31
31
  - Use with or without gui (receive and send messages via command line commands)
32
32
  - GUI provides simultaneous views of odd and even cycles
33
- - GUI shows worked-before info on CQ calls (not yet on PyPi)
33
+ - GUI shows worked-before info on CQ calls
34
34
  - Messages overlaid on waterfall signals that produce them
35
35
  - Automatically chooses clearest Tx frequency
36
36
  - Modern programming language throughout
37
37
  - Finds sound cards by keywords so follows them if windows moves them ...
38
+ - Logs QSOs to ADIF file and all spots to WSJTX-style ALL.txt file
39
+ - Uploads spots to pskreporter
40
+
41
+ To enable uploading of spots to pskreporter, make sure that your .ini file includes
42
+ ```
43
+ [pskreporter]
44
+ upload = Y
45
+ ```
38
46
 
39
47
  <img width="1003" height="1020" alt="image" src="https://github.com/user-attachments/assets/bf6e3f78-531a-4c9b-ab2b-b51cc04ad980" />
40
48
 
@@ -112,6 +120,7 @@ Note - section 9 of the QEX paper states that the above two WSJT-X resources are
112
120
  **FT8 decoding explorations / explanations**
113
121
  - [VK3JPK's FT8 notes](https://github.com/vk3jpk/ft8-notes) including comprehensive [Python source code](https://github.com/vk3jpk/ft8-notes/blob/master/ft8.py)
114
122
  - [G4JNT notes on LDPC coding process](http://www.g4jnt.com/WSJT-X_LdpcModesCodingProcess.pdf)
123
+ - [Kristian Glass's 2025-05-06 Understanding the FT8 binary protocol](https://notes.doismellburning.co.uk/notebook/2025-05-06-understanding-the-ft8-binary-protocol/)
115
124
 
116
125
  **FT8 decoding in hardware**
117
126
  - [Optimizing the (Web-888) FT8 Skimmer Experience](https://www.rx-888.com/web/design/digi.html) (see also [RX-888 project](https://www.rx-888.com/) )
@@ -122,7 +131,8 @@ Note - section 9 of the QEX paper states that the above two WSJT-X resources are
122
131
  - [Post about ft8play](https://groups.io/g/FT8-Digital-Mode/topic/i_made_a_thing_ft8play/107846361)
123
132
 
124
133
  **Browser-based decoder/encoders**
125
- - [ft8js](https://e04.github.io/ft8js/example/browser/index.html) - source [github](https://github.com/e04/ft8js?tab=readme-ov-file), uses [FT8_lib](https://github.com/kgoba/ft8_lib)
134
+ - [ft8js by e04](https://e04.github.io/ft8js/example/browser/index.html) - source [github](https://github.com/e04/ft8js?tab=readme-ov-file), uses web-assembled version of [FT8_lib](https://github.com/kgoba/ft8_lib)
135
+ - [ft8ts by e04](https://github.com/e04/ft8ts), - pure TypeScript implementation
126
136
  - [ChromeFT8 Browser Extension](https://github.com/Transwarp8/ChromeFT8), decoder adapted from [ft8js](https://e04.github.io/ft8js/example/browser/index.html)
127
137
 
128
138
  <script data-goatcounter="https://g1ojs-github.goatcounter.com/count" async src="//gc.zgo.at/count.js"></script>
@@ -1,5 +1,5 @@
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
- # All-Python FT8 Transceiver(WIP) GUI / Command Line Modem
2
+ # All-Python FT8 Transceiver GUI / Command Line Modem
3
3
 
4
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
 
@@ -11,11 +11,19 @@ If you're interested in how this works, maybe have a look at [MiniPyFT8](https:/
11
11
  - Doesn't try to do everything, so launches quickly (~2 seconds on my old Dell Optiplex 790)
12
12
  - Use with or without gui (receive and send messages via command line commands)
13
13
  - GUI provides simultaneous views of odd and even cycles
14
- - GUI shows worked-before info on CQ calls (not yet on PyPi)
14
+ - GUI shows worked-before info on CQ calls
15
15
  - Messages overlaid on waterfall signals that produce them
16
16
  - Automatically chooses clearest Tx frequency
17
17
  - Modern programming language throughout
18
18
  - Finds sound cards by keywords so follows them if windows moves them ...
19
+ - Logs QSOs to ADIF file and all spots to WSJTX-style ALL.txt file
20
+ - Uploads spots to pskreporter
21
+
22
+ To enable uploading of spots to pskreporter, make sure that your .ini file includes
23
+ ```
24
+ [pskreporter]
25
+ upload = Y
26
+ ```
19
27
 
20
28
  <img width="1003" height="1020" alt="image" src="https://github.com/user-attachments/assets/bf6e3f78-531a-4c9b-ab2b-b51cc04ad980" />
21
29
 
@@ -93,6 +101,7 @@ Note - section 9 of the QEX paper states that the above two WSJT-X resources are
93
101
  **FT8 decoding explorations / explanations**
94
102
  - [VK3JPK's FT8 notes](https://github.com/vk3jpk/ft8-notes) including comprehensive [Python source code](https://github.com/vk3jpk/ft8-notes/blob/master/ft8.py)
95
103
  - [G4JNT notes on LDPC coding process](http://www.g4jnt.com/WSJT-X_LdpcModesCodingProcess.pdf)
104
+ - [Kristian Glass's 2025-05-06 Understanding the FT8 binary protocol](https://notes.doismellburning.co.uk/notebook/2025-05-06-understanding-the-ft8-binary-protocol/)
96
105
 
97
106
  **FT8 decoding in hardware**
98
107
  - [Optimizing the (Web-888) FT8 Skimmer Experience](https://www.rx-888.com/web/design/digi.html) (see also [RX-888 project](https://www.rx-888.com/) )
@@ -103,7 +112,8 @@ Note - section 9 of the QEX paper states that the above two WSJT-X resources are
103
112
  - [Post about ft8play](https://groups.io/g/FT8-Digital-Mode/topic/i_made_a_thing_ft8play/107846361)
104
113
 
105
114
  **Browser-based decoder/encoders**
106
- - [ft8js](https://e04.github.io/ft8js/example/browser/index.html) - source [github](https://github.com/e04/ft8js?tab=readme-ov-file), uses [FT8_lib](https://github.com/kgoba/ft8_lib)
115
+ - [ft8js by e04](https://e04.github.io/ft8js/example/browser/index.html) - source [github](https://github.com/e04/ft8js?tab=readme-ov-file), uses web-assembled version of [FT8_lib](https://github.com/kgoba/ft8_lib)
116
+ - [ft8ts by e04](https://github.com/e04/ft8ts), - pure TypeScript implementation
107
117
  - [ChromeFT8 Browser Extension](https://github.com/Transwarp8/ChromeFT8), decoder adapted from [ft8js](https://e04.github.io/ft8js/example/browser/index.html)
108
118
 
109
119
  <script data-goatcounter="https://g1ojs-github.goatcounter.com/count" async src="//gc.zgo.at/count.js"></script>
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "PyFT8"
3
- version = "2.3.0"
3
+ version = "2.3.1"
4
4
  license = "GPL-3.0-or-later"
5
5
 
6
6
  authors = [
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