PyFT8 2.3.0__tar.gz → 2.4.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.3.0 → pyft8-2.4.0}/PKG-INFO +14 -4
  2. pyft8-2.4.0/PyFT8/callhashes.py +19 -0
  3. {pyft8-2.3.0 → pyft8-2.4.0}/PyFT8/pskr_upload.py +22 -18
  4. {pyft8-2.3.0 → pyft8-2.4.0}/PyFT8/pyft8.py +33 -15
  5. {pyft8-2.3.0 → pyft8-2.4.0}/PyFT8/receiver.py +39 -14
  6. {pyft8-2.3.0 → pyft8-2.4.0}/PyFT8/transmitter.py +28 -3
  7. {pyft8-2.3.0 → pyft8-2.4.0}/PyFT8.egg-info/PKG-INFO +14 -4
  8. {pyft8-2.3.0 → pyft8-2.4.0}/PyFT8.egg-info/SOURCES.txt +1 -0
  9. {pyft8-2.3.0 → pyft8-2.4.0}/README.md +13 -3
  10. {pyft8-2.3.0 → pyft8-2.4.0}/pyproject.toml +1 -1
  11. {pyft8-2.3.0 → pyft8-2.4.0}/LICENSE +0 -0
  12. {pyft8-2.3.0 → pyft8-2.4.0}/MANIFEST.in +0 -0
  13. {pyft8-2.3.0 → pyft8-2.4.0}/PyFT8/__init__.py +0 -0
  14. {pyft8-2.3.0 → pyft8-2.4.0}/PyFT8/gui.py +0 -0
  15. {pyft8-2.3.0 → pyft8-2.4.0}/PyFT8/rigctrl.py +0 -0
  16. {pyft8-2.3.0 → pyft8-2.4.0}/PyFT8/time_utils.py +0 -0
  17. {pyft8-2.3.0 → pyft8-2.4.0}/PyFT8.egg-info/dependency_links.txt +0 -0
  18. {pyft8-2.3.0 → pyft8-2.4.0}/PyFT8.egg-info/entry_points.txt +0 -0
  19. {pyft8-2.3.0 → pyft8-2.4.0}/PyFT8.egg-info/requires.txt +0 -0
  20. {pyft8-2.3.0 → pyft8-2.4.0}/PyFT8.egg-info/top_level.txt +0 -0
  21. {pyft8-2.3.0 → pyft8-2.4.0}/setup.cfg +0 -0
  22. {pyft8-2.3.0 → pyft8-2.4.0}/tests/dev/osd.py +0 -0
  23. {pyft8-2.3.0 → pyft8-2.4.0}/tests/dev/test_generate_wav.py +0 -0
  24. {pyft8-2.3.0 → pyft8-2.4.0}/tests/dev/test_loopback_performance.py +0 -0
  25. {pyft8-2.3.0 → pyft8-2.4.0}/tests/plot_baseline.py +0 -0
  26. {pyft8-2.3.0 → pyft8-2.4.0}/tests/spare.py +0 -0
  27. {pyft8-2.3.0 → pyft8-2.4.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.0
3
+ Version: 2.4.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
@@ -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>
@@ -0,0 +1,19 @@
1
+ call_hashes = {}
2
+
3
+ def add_call_hashes(call):
4
+ global call_hashes
5
+ chars = " 0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ/"
6
+ call_padded = (call + " ")[:11]
7
+ hashes = []
8
+ for m in [10,12,22]:
9
+ x = 0
10
+ for c in call_padded:
11
+ x = 38*x + chars.find(c)
12
+ x = x & ((int(1) << 64) - 1)
13
+ x = x & ((1 << 64) - 1)
14
+ x = x * 47055833459
15
+ x = x & ((1 << 64) - 1)
16
+ x = x >> (64 - m)
17
+ hashes.append(x)
18
+ call_hashes[(x, m)] = call
19
+ return hashes
@@ -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,15 +59,18 @@ 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}, {mode}, {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))
65
67
  struct.pack_into("!H", packet, 2, len(packet))
66
68
  self.seq += len(self.reports)
67
69
  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
+ txt = f"[pskr_upload] Sent packet with {len(self.reports)} reports"
71
+ print(txt)
72
+ self.console_print(txt)
73
+ self.reports = {}
70
74
 
71
75
 
72
76
  #pskr = PSKReporter('G1OJS', 'IO90ju', software = 'PyFT8', tt = int(time.time()))
@@ -11,11 +11,12 @@ 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.4.0'
15
+
14
16
  MAX_TX_START_SECONDS = 2.5
15
- T_CYC = 15
16
17
  rig, gui, qso, worked_before, pskr_upload = None, None, None, None, None
17
18
 
18
- def get_config(config_folder):
19
+ def get_config():
19
20
  import configparser
20
21
  global config
21
22
  config = configparser.ConfigParser()
@@ -41,7 +42,7 @@ def parse_from_adif_rec(rec, field):
41
42
  return rec[p2+1: p2+1+n]
42
43
 
43
44
  class Logging:
44
- def __init__(self, config_folder):
45
+ def __init__(self):
45
46
  self.adif_log_file = f"{config_folder}/PyFT8.adi"
46
47
  self.worked_before_file = f"{config_folder}/PyFT8_wb.pkl"
47
48
  if(not os.path.exists(self.adif_log_file)):
@@ -117,6 +118,10 @@ class Message:
117
118
  def wsjtx_screen_format(self):
118
119
  return f"{self.cyclestart['string']} {self.snr:+03d} {self.dt:4.1f} {self.fHz:4.0f} ~ {self.msg}"
119
120
 
121
+ def wsjtx_all_txt_format(self):
122
+ fMHz = float(qso.band_info['fMHz']) if qso.band_info['fMHz'] is not None else 0
123
+ return f"{self.cyclestart['string']} {fMHz:8.3f} Rx FT8 {self.snr:+03d} {self.dt:4.1f} {self.fHz:4.0f} ~ {self.msg}"
124
+
120
125
 
121
126
  class FT8_QSO:
122
127
  def __init__(self, logging):
@@ -158,14 +163,17 @@ class FT8_QSO:
158
163
  if self.tx_cycle is None:
159
164
  self.tx_cycle = global_time_utils.curr_cycle_from_time()
160
165
  self.tx_freq = clear_frequencies[self.tx_cycle]
161
- console_print(f"[PyFT8] Set tx cycle = {self.tx_cycle} f = {self.tx_freq}")
162
- console_print(f"Transmitting {self.message_to_transmit} on cycle {self.tx_cycle}")
166
+ console_print(f"[PyFT8] Set tx cycle = {self.tx_cycle} f = {self.tx_freq:5.1f}")
163
167
  symbols = audio_out.create_ft8_symbols(self.message_to_transmit)
164
- audio_data = audio_out.create_ft8_wave(symbols, f_base = self.tx_freq)
165
- rig.ptt_on()
166
- audio_out.play_data_to_soundcard(audio_data, output_device_idx)
167
- rig.ptt_off()
168
- self.last_tx = self.message_to_transmit
168
+ if any(symbols):
169
+ console_print(f"Transmitting {self.message_to_transmit} on cycle {self.tx_cycle}")
170
+ audio_data = audio_out.create_ft8_wave(symbols, f_base = self.tx_freq)
171
+ rig.ptt_on()
172
+ audio_out.play_data_to_soundcard(audio_data, output_device_idx)
173
+ rig.ptt_off()
174
+ self.last_tx = self.message_to_transmit
175
+ else:
176
+ console_print(f"Couldn't encode message {self.message_to_transmit}", color = 'red') # move this to earlier by setting tx symbols not tx message
169
177
  self.message_to_transmit = None
170
178
 
171
179
  def log(self):
@@ -234,6 +242,13 @@ def wait_for_keyboard():
234
242
  except KeyboardInterrupt:
235
243
  pass
236
244
 
245
+ def write_all_txt_row(message):
246
+ all_file = f"{config_folder}/ALL.txt"
247
+ mode = 'w' if not os.path.exists(all_file) else 'a'
248
+ row = message.wsjtx_all_txt_format()
249
+ with open(all_file, mode) as f:
250
+ f.write(f"{row}\n")
251
+
237
252
  #============= Callbacks for GUI ==========================================================
238
253
  def on_decode(c):
239
254
  message = Message(c)
@@ -242,8 +257,9 @@ def on_decode(c):
242
257
  if qso.band_info['b'] is not None and pskr_upload is not None:
243
258
  dx_call = c.msg_tuple[1]
244
259
  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()))
260
+ pskr_upload.add_report(dx_call, int(1000000*float(qso.band_info['fMHz'])) + c.fHz, c.snr, 'FT8', 1, int(time.time()))
246
261
  print(message.wsjtx_screen_format())
262
+ write_all_txt_row(message)
247
263
 
248
264
  def on_busy_profile(busy_profile, cycle):
249
265
  if output_device_idx is None:
@@ -253,6 +269,8 @@ def on_busy_profile(busy_profile, cycle):
253
269
  idx = np.argmin(busy_profile[f0_idx:fn_idx])
254
270
  clear_frequencies[cycle] = (f0_idx + idx) * audio_in.df
255
271
  console_print(f"[on_busy] Set Tx freq to {clear_frequencies[cycle]:6.1f} for cycle {cycle}")
272
+ if qso.band_info['b'] is None:
273
+ console_print(f"[PyFT8] Band not set; please select a band.", color = 'red')
256
274
 
257
275
  def on_control_click(btn_widg):
258
276
  btn_def = btn_widg.user_data
@@ -283,7 +301,7 @@ def console_print(text, color = 'white'):
283
301
  print(text)
284
302
 
285
303
  def cli():
286
- global audio_in, audio_out, output_device_idx, rig, gui, qso, config, clear_frequencies, pskr_upload
304
+ global audio_in, audio_out, output_device_idx, rig, gui, qso, config, config_folder, clear_frequencies, pskr_upload
287
305
  import time
288
306
  parser = argparse.ArgumentParser(prog='PyFT8rx', description = 'Command Line FT8 decoder')
289
307
  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 +315,12 @@ def cli():
297
315
 
298
316
  output_device_idx = None
299
317
  config_folder = f"{args.config_folder}".strip()
300
- get_config(config_folder)
301
- logging = Logging(config_folder)
318
+ get_config()
319
+ logging = Logging()
302
320
  mc, mg = config['station']['call'], config['station']['grid']
303
321
  if mc is not None and 'pskreporter' in config.keys():
304
322
  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
323
+ 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
324
  console_print(f"[PyFT8] Spots will upload to pskreporter")
307
325
  qso = FT8_QSO(logging)
308
326
  rig = Rig(config)
@@ -6,6 +6,7 @@ from PyFT8.time_utils import global_time_utils, Ticker
6
6
  import os
7
7
  import pyaudio
8
8
  import pickle
9
+ from PyFT8.callhashes import call_hashes, add_call_hashes
9
10
 
10
11
  T_CYC = 15
11
12
  HPS = 4
@@ -31,7 +32,6 @@ HOPS_PER_GRID = 2 * HOPS_PER_CYCLE
31
32
 
32
33
  global_time_utils.set_cycle_length(T_CYC)
33
34
 
34
-
35
35
  #=========== Unpacking functions ========================================
36
36
  def get_bits(bits, n):
37
37
  mask = (1 << n) - 1
@@ -40,45 +40,70 @@ def get_bits(bits, n):
40
40
  return out, bits
41
41
 
42
42
  def unpack(bits):
43
+ # print(f"{bits:77b}")
43
44
  i3, bits = get_bits(bits,3)
45
+ # print(i3)
44
46
  if i3 == 0:
45
47
  n3, bits = get_bits(bits,3)
46
48
  if n3 == 0:
47
49
  return ('Free text','not','implemented')
48
50
  else:
49
51
  return (['DXpedition','Field Day', 'Field Day', 'Telemetry'][n3-1],'not','implemented')
50
- elif i3 == 1:
52
+ elif i3 == 1 or i3 == 2: # 1 = Std Msg incl /R 2 = 'EU VHF' = Std Msg incl /P
51
53
  gr, bits = get_bits(bits,16)
52
54
  cb, bits = get_bits(bits,29)
53
55
  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')
56
+ return (call_28(ca, i3), call_28(cb, i3), decode_grid(gr))
57
57
  elif i3 == 3:
58
58
  return ('RTTY RU','not','implemented')
59
59
  elif i3 == 4:
60
- return ('Nonstd Call','not','implemented')
60
+ cq_, bits = get_bits(bits,1)
61
+ rrr, bits = get_bits(bits,2)
62
+ swp, bits = get_bits(bits,1)
63
+ c58, bits = get_bits(bits,58)
64
+ hsh, bits = get_bits(bits,12)
65
+ ca = "CQ" if cq_ else call_hashes.get((hsh,12), '<....>')
66
+ cb = call_58(c58)
67
+ (ca, cb) = (cb, ca) if swp else (ca, cb)
68
+ return (ca, cb, ('', 'RRR', 'RR73', '73')[rrr])
61
69
  elif i3 == 5:
62
70
  return ('EU VHF','not','implemented')
63
71
 
64
- def decode_call(call_int):
72
+ def call_58(call_int):
73
+ call = ""
74
+ chars = " 0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ/"
75
+ for i in range(12):
76
+ call = chars[call_int % 38] + call
77
+ call_int = call_int // 38
78
+ call = call.strip()
79
+ add_call_hashes(call)
80
+ return call
81
+
82
+ def call_28(call_int, i3):
83
+ def get_table_7(call_int):
84
+ table_7 = {'DE':(0,0),'QRZ':(1,1),'CQ':(2,2), 'CQ nnn':(3,1002),'CQ x':(1004,1029),
85
+ 'CQ xx':(1031,1731),'CQ xxxx':(21443,532443),'hash':(2063592,2063592+4194303)}
86
+ for ct, (lo, hi) in table_7.items():
87
+ if lo <= call_int <= hi:
88
+ return ct
65
89
  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
90
  call_fields = [ (' ' + digs + ltrs, 36*10*27**3), (digs + ltrs, 10*27**3), (digs + ' ' * 17, 27**3),
69
91
  (' ' + ltrs, 27**2), (' ' + ltrs, 27), (' ' + ltrs, 1) ]
70
- portable = call_int & 1
92
+ portable_rover = call_int & 1
71
93
  call_int >>= 1
72
- for ct, (lo, hi) in table_7.items():
73
- if lo <= call_int <= hi:
74
- return ct
94
+ t7 = get_table_7(call_int)
95
+ if t7 is not None:
96
+ return t7 if t7 != 'hash' else call_hashes.get((call_int - 2063592, 22), '<....>')
75
97
  call_int -= (2063592 + 4194304)
76
98
  chars = []
77
99
  for alphabet, div in call_fields:
78
100
  idx, call_int = divmod(call_int, div)
79
101
  chars.append(alphabet[idx])
80
102
  call = ''.join(chars).strip()
81
- return call + '/P' if portable else call
103
+ if portable_rover:
104
+ call = call + ('/P' if i3 == 2 else '/R')
105
+ add_call_hashes(call)
106
+ return call
82
107
 
83
108
  def decode_grid(grid_int):
84
109
  g15 = grid_int & 0x7FFF
@@ -2,6 +2,7 @@ import numpy as np
2
2
  import wave
3
3
  import pyaudio
4
4
  import time
5
+ from PyFT8.callhashes import call_hashes, add_call_hashes
5
6
 
6
7
  #==================== AUDIO OUT ================================================================
7
8
 
@@ -67,21 +68,45 @@ def _pack_message(c1, c2, gr):
67
68
  c28a, p1a = pack_ft8_c28(c1)
68
69
  c28b, p1b = pack_ft8_c28(c2)
69
70
  g15, ir = pack_ft8_g15(gr)
70
- i3 = 1
71
+ i3 = 2 if p1a or p1b else 1
71
72
  n3 = 0
72
73
  symbols, bits77 = [], 0
73
74
  if(c28a>=0 and c28b>=0):
74
75
  bits77 = (c28a<<28+1+1+1+15+3) | (p1a<<28+1+1+15+3) | (c28b<<1+1+15+3) | (p1b <<1+15+3) | (ir<<15+3) | (g15<< 3) | (i3)
75
76
  symbols = encode_bits77(bits77)
77
+ if not any(symbols):
78
+ i3 = 4
79
+ full_call = c1 if c28b>0 else c2
80
+ hash_call = c2 if c28b>0 else c1
81
+ c58 = pack_ft8_c58(full_call)
82
+ hashes = add_call_hashes(hash_call)
83
+ swp = 1 if hash_call == c2 else 0
84
+ rrr, cq_ = 0, 0
85
+ if gr in ('RRR', 'RR73', '73'):
86
+ rrr = ('', 'RRR', 'RR73', '73').index(gr)
87
+ bits77 = (hashes[1]<<58+1+2+1+3) | (c58 <<1+2+1+3) | (swp<<2+1+3) | (rrr<<1+3) | (cq_<<3) | (i3)
88
+ symbols = encode_bits77(bits77)
76
89
  return symbols, bits77
77
90
 
91
+
92
+ def pack_ft8_c58(call):
93
+ chars = " 0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ/"
94
+ n58=0
95
+ call = (call + " ")[:11]
96
+ for i in range(0,11):
97
+ n58 = n58*38 + chars.index(call[i])
98
+ return n58
99
+
78
100
  def pack_ft8_c28(call):
101
+ if '/' in call and not call.endswith("P") and not call.endswith("R"): return -1, 0
79
102
  tkns = ['DE','QRZ','CQ']
80
103
  if (call in tkns):
81
104
  c28, p1 = tkns.index(call), 0
82
105
  else:
83
106
  p1 = 1 if call[-2:] == '/P' else 0
84
107
  call = call.replace('/P','')
108
+ if len(call) > 6:
109
+ return -1, 0
85
110
  prepend_space = '' if call[2].isdigit() else ' '
86
111
  call = (prepend_space + call + ' ')[:6]
87
112
  a = ' 0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ'
@@ -160,13 +185,13 @@ def append_crc(bits77_int):
160
185
  if __name__ == "__main__":
161
186
  OK = True
162
187
  msgs = [("G1OJS/P", "G1OJS/P", "IO90"),("WM3PEN","EA6VQ","+08"),("E67A/P","EA6VQ","R-08"),
163
- ("CQ","CT7ARQ/P","RRR"), ("EC5A","9A5E","RR73"), ("EC5A","9A5E","73")]
188
+ ("CQ","CT7ARQ/P","RRR"), ("EC5A","9A5E","RR73"), ("EC5A/P","9A5E","73"), ("EC5A/MM","9A5E","73")]
164
189
  for msg_tx in msgs:
165
190
  symbols, bits77 = _pack_message(*msg_tx)
166
191
  from PyFT8.receiver import unpack
167
192
  msg_rx = unpack(bits77)
168
193
  print(f"\n{msg_tx}\n{msg_rx}")
169
- OK = OK and (msg_tx == msg_rx)
194
+ OK = OK and (msg_tx == msg_rx) or 'implemented' in msg_rx
170
195
  #print(''.join([str(s) for s in symbols]))
171
196
  print("\nPASSED" if OK else "\nFAILED")
172
197
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: PyFT8
3
- Version: 2.3.0
3
+ Version: 2.4.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
@@ -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>
@@ -3,6 +3,7 @@ MANIFEST.in
3
3
  README.md
4
4
  pyproject.toml
5
5
  PyFT8/__init__.py
6
+ PyFT8/callhashes.py
6
7
  PyFT8/gui.py
7
8
  PyFT8/pskr_upload.py
8
9
  PyFT8/pyft8.py
@@ -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.4.0"
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