PyFT8 2.12.3__tar.gz → 3.0.2__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 (30) hide show
  1. {pyft8-2.12.3 → pyft8-3.0.2}/PKG-INFO +11 -22
  2. {pyft8-2.12.3 → pyft8-3.0.2}/PyFT8/databases.py +18 -19
  3. {pyft8-2.12.3 → pyft8-3.0.2}/PyFT8/pskreporter.py +2 -1
  4. {pyft8-2.12.3 → pyft8-3.0.2}/PyFT8/pyft8.py +3 -9
  5. pyft8-3.0.2/PyFT8/rigctrl.py +43 -0
  6. {pyft8-2.12.3 → pyft8-3.0.2}/PyFT8/transmitter.py +68 -36
  7. {pyft8-2.12.3 → pyft8-3.0.2}/PyFT8.egg-info/PKG-INFO +11 -22
  8. {pyft8-2.12.3 → pyft8-3.0.2}/README.md +10 -21
  9. {pyft8-2.12.3 → pyft8-3.0.2}/pyproject.toml +1 -1
  10. pyft8-2.12.3/PyFT8/rigctrl.py +0 -102
  11. {pyft8-2.12.3 → pyft8-3.0.2}/LICENSE +0 -0
  12. {pyft8-2.12.3 → pyft8-3.0.2}/MANIFEST.in +0 -0
  13. {pyft8-2.12.3 → pyft8-3.0.2}/PyFT8/__init__.py +0 -0
  14. {pyft8-2.12.3 → pyft8-3.0.2}/PyFT8/gui.py +0 -0
  15. {pyft8-2.12.3 → pyft8-3.0.2}/PyFT8/receiver.py +0 -0
  16. {pyft8-2.12.3 → pyft8-3.0.2}/PyFT8/time_utils.py +0 -0
  17. {pyft8-2.12.3 → pyft8-3.0.2}/PyFT8.egg-info/SOURCES.txt +0 -0
  18. {pyft8-2.12.3 → pyft8-3.0.2}/PyFT8.egg-info/dependency_links.txt +0 -0
  19. {pyft8-2.12.3 → pyft8-3.0.2}/PyFT8.egg-info/entry_points.txt +0 -0
  20. {pyft8-2.12.3 → pyft8-3.0.2}/PyFT8.egg-info/requires.txt +0 -0
  21. {pyft8-2.12.3 → pyft8-3.0.2}/PyFT8.egg-info/top_level.txt +0 -0
  22. {pyft8-2.12.3 → pyft8-3.0.2}/setup.cfg +0 -0
  23. {pyft8-2.12.3 → pyft8-3.0.2}/tests/dev/CQ AAAA.py +0 -0
  24. {pyft8-2.12.3 → pyft8-3.0.2}/tests/dev/osd.py +0 -0
  25. {pyft8-2.12.3 → pyft8-3.0.2}/tests/dev/test_generate_wav.py +0 -0
  26. {pyft8-2.12.3 → pyft8-3.0.2}/tests/dev/test_loopback_performance.py +0 -0
  27. {pyft8-2.12.3 → pyft8-3.0.2}/tests/dev/view_worked_before.py +0 -0
  28. {pyft8-2.12.3 → pyft8-3.0.2}/tests/plot_baseline.py +0 -0
  29. {pyft8-2.12.3 → pyft8-3.0.2}/tests/spare.py +0 -0
  30. {pyft8-2.12.3 → pyft8-3.0.2}/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.12.3
3
+ Version: 3.0.2
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
@@ -27,7 +27,8 @@ PyFT8 is somewhat experimental, with a focus on demonstrating FT8 written in Pyt
27
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
28
 
29
29
  ## Features
30
- - Rx and Tx of standard messages with optional /P and /R, and nonstandard calls plus hashed calls
30
+ - Rx and Tx of standard messages with optional /P and /R
31
+ - RX of nonstandard calls plus hashed calls
31
32
  - Launches quickly (~2 seconds on my old Dell Optiplex 790)
32
33
  - Use with or without gui (receive and send messages via command line commands)
33
34
  - Automatically chooses clearest Tx frequency
@@ -35,14 +36,13 @@ If you're interested in how this works, maybe have a look at [MiniPyFT8](https:/
35
36
  - Finds sound cards by keywords so follows them if windows moves them ...
36
37
  - Logs QSOs to ADIF file and all spots to WSJTX-style ALL.txt file
37
38
  - Uploads spots to pskreporter
38
- - Direct CAT control for some rigs, designed to drop connection when not used, allowing sharing of rig's serial port
39
- - Or control rigs via Hamlib
39
+ - Control rigs via Hamlib
40
40
 
41
41
  The Gui shows:
42
42
  - Simultaneous views of odd and even cycles
43
43
  - Messages overlaid on waterfall signals that produce them
44
44
  - Worked-before info and fine grid locators / distance and bearing in the message boxes
45
- - List of stations hearing your transmissions on the selected band
45
+ - List of stations hearing your transmissions on the selected band, alternating with list of stations you're hearing. On these lists, 'new' callsigns (first heard since program launch) are in white font.
46
46
  - Marker on message if 2nd call is hearing you
47
47
  - Band activity in your level 4 square live updated next to band select buttons
48
48
  - Number of remote stations hearing your Tx, number of remote Txs that you're hearing, plus the same info for the 'best' station in your level 4 square
@@ -63,7 +63,7 @@ If you want to install this software without getting into the code, you can inst
63
63
  pip install PyFT8
64
64
  ```
65
65
 
66
- Once installed, you can use the following commands to run it. Otherwise, please download or browse the code, or fork the repo and play with it! If you do fork it, please check back here as I'm constantly (as of Jan 2026) rewriting and improving.
66
+ Once installed, you can use the following commands to run it. Otherwise, please download or browse the code, or fork the repo and play with it! If you do fork it, please check back here as I'm constantly (as of April 2026) rewriting and improving.
67
67
 
68
68
  The use cases below are the ones held in the test cases at the bottom of the main pyft8.py file. Where a config folder and .ini file is required, if you leave off the '-c {folder}' option, you'll be asked if you want to create a .ini file in the current directory.
69
69
 
@@ -77,30 +77,19 @@ The use cases below are the ones held in the test cases at the bottom of the mai
77
77
  | Command line create a wav file | pyft8 -w "Mywav.wav" -m "CQ G1OJS IO90"| No config folder / .ini file required |
78
78
 
79
79
  ### Rig control
80
- I've included the Python code that I use with my Icom IC-7100 in the file 'rigctrl.py', and believe I've moved sufficient 'specification' for the rig protocol into the .ini file so that you can paste in your own rig specification (see for e.g. the Omnirig .ini file for your rig) and get it working with PyFT8 controlling PTT and frequency. I designed this code to drop the serial connection when it's not required, so that the rig's serial port can be accessed by other software at the same time (assuming that the other software returns the favour and doesn't permanently hog the serial port).
81
-
82
- I've also included a basic Hamlib interface which launches rigctld and uses that to control the rig. To use this, make sure that the hamlib section of the ini file is populated; this will then take precedence over the direct CAT control section.
83
-
84
- Alternatively, you can run PyFT8 without rig control; if there is no rig found, PyFT8 defaults to running without a rig connected. In this case, you need to provide your own PTT method and note that the band buttons will only set the information used for logging QSOs to the PyFT8.adi file. Or you can use PyFT8 as Rx-only.
80
+ I designed PyFT8 to be minimal as far as possible, and it needs very limited control of the rig in order to transmit. However, whilst previous versions used direct CAT control and Hamlib as a fallback, Version 3.0.0 moves to Hamlib as the only interface.
85
81
 
86
82
  ## Customising
87
- Even if you don't want to edit the code, there's quite a bit that you can change in the .ini file. I haven't written a user guide yet, so here's my own .ini file for reference. I use CAT control so my hamlib block is disabled by adding a _ to the name of the block (I keep it there for testing only). I need to add some more switches really to catch up with recent additions.
83
+ Even if you don't want to edit the code, there's quite a bit that you can change in the .ini file. I haven't written a user guide yet, so here's my own .ini file for reference. You'll note that my instance of rigctld is in C:/WSJT/wsjtx/bin/rigctld-wsjtx, but any rigctld should also work. PyFT8 launches this if it's not already running (to be precise, it launches it if it can't open a socket on localhost:4532).
88
84
 
89
85
  ```
90
86
  [station]
91
87
  call = G1OJS
92
- grid = IO90******
88
+ grid = IO******
93
89
  [gui]
94
90
  loc = km_deg
95
91
  wb = Y
96
- [rig]
97
- port = COM4
98
- baud_rate = 9600
99
- set_freq_command = FEFE88E0.05.0000000000.FD
100
- set_freq_value = 5|5|vfBcdLU|1|0
101
- ptt_on_command = FEFE88E0.1C00.01.FD
102
- ptt_off_command = FEFE88E0.1C00.00.FD
103
- [_hamlib_rig]
92
+ [hamlib_rig]
104
93
  rigctld = C:/WSJT/wsjtx/bin/rigctld-wsjtx
105
94
  port = COM4
106
95
  baud_rate = 9600
@@ -139,7 +128,7 @@ PyFT8 doesn't decode / encode *all* message types. The table below shows which a
139
128
  |1|Std Msg |Y| Y |Standard <=6 char callsigns, can include /R |
140
129
  |2|EU VHF |Y|Y| Standard <=6 char callsigns, can include /P |
141
130
  |3|RTTY RU | | | |
142
- |4|NonStd Call |Y|Y| <=11 char callsigns + hashed call|
131
+ |4|NonStd Call |Y|N| <=11 char callsigns + hashed call|
143
132
  |5|EU VHF | | | |
144
133
 
145
134
  ## Acknowledgements
@@ -1,7 +1,7 @@
1
1
  from PyFT8.pskreporter import PSKR_MQTT_listener
2
2
  import threading, time, os, pickle, json
3
3
 
4
- call_hashes = {}
4
+ call_hashes, hashes_for_calls = {}, {}
5
5
 
6
6
  def add_call_hashes(call):
7
7
  global call_hashes
@@ -17,9 +17,9 @@ def add_call_hashes(call):
17
17
  x = x * 47055833459
18
18
  x = x & ((1 << 64) - 1)
19
19
  x = x >> (64 - m)
20
- hashes.append(x)
20
+ hashes.append((x, m))
21
21
  call_hashes[(x, m)] = call
22
- return hashes
22
+ hashes_for_calls[call] = hashes
23
23
 
24
24
  def grid_to_latlong(grid, centre = True):
25
25
  lat, lon = -90, -180
@@ -80,7 +80,6 @@ class DiskDict:
80
80
 
81
81
  class History:
82
82
  def __init__(self, config_folder, my_call, home_square, pskr_refresh_mins, parse_all_file):
83
- self.t0 = 1775324204
84
83
  self.pskr_refresh_mins = pskr_refresh_mins
85
84
  self.config_folder = config_folder
86
85
  self.my_call = my_call
@@ -93,12 +92,12 @@ class History:
93
92
  self.hearing_me_new = {}
94
93
  self.heard_by_me_new = {}
95
94
  self.call_to_grid = DiskDict(f"{self.config_folder}/call_to_grid.json", 7) # all time cache call -> fine locator
96
- self.band_TxRx_homecall_report_times = DiskDict(f"{self.config_folder}/report_times.json", 9) # last 20 mins data -> per band tx/rx & current band detail
95
+ self.band_TxRx_homecall_recent_l4grid = DiskDict(f"{self.config_folder}/recent_l4grid.json", 9) # last 20 mins data -> per band tx/rx & current band detail
97
96
  self.home_activity = {}
98
97
  self.home_most_remotes = {}
99
98
  self.lock = threading.Lock()
100
99
  if parse_all_file:
101
- self.load_all_file(f"{self.config_folder}/ALL.txt")
100
+ self.load_hearing_heard_from_all_file(f"{self.config_folder}/ALL.txt")
102
101
  self.hearing_me.save()
103
102
  self.heard_by_me.save()
104
103
  print("All file parsed and saved to hearing_me / heard_by_me files")
@@ -123,7 +122,7 @@ class History:
123
122
  else:
124
123
  print(m)
125
124
 
126
- def load_all_file(self, all_file):
125
+ def load_hearing_heard_from_all_file(self, all_file):
127
126
  recs = self.parse_all_txt(all_file)
128
127
  if not any(recs): return
129
128
  for r in recs:
@@ -179,8 +178,8 @@ class History:
179
178
  self.call_to_grid.data[call] = grid
180
179
 
181
180
  def add_homespots_record(self, key, t):
182
- self.band_TxRx_homecall_report_times.data.setdefault(key, [])
183
- self.band_TxRx_homecall_report_times.data[key].append(int(t-self.t0))
181
+ self.band_TxRx_homecall_recent_l4grid.data.setdefault(key, [])
182
+ self.band_TxRx_homecall_recent_l4grid.data[key].append(int(t))
184
183
 
185
184
  def add_myspots_record(self, historic_data, new_alert_data, band, call, t, rp):
186
185
  self._update_new_alert(band, call, historic_data, new_alert_data)
@@ -227,29 +226,29 @@ class History:
227
226
  self.home_most_remotes[b] = [('Nobody',0), ('Nobody',0)]
228
227
 
229
228
  # keep only the remote spots that happened in the self.pskr_refresh_mins window
230
- for band_TxRx_homecall in self.band_TxRx_homecall_report_times.data:
231
- band_TxRx_homecall_report_times = self.band_TxRx_homecall_report_times.data[band_TxRx_homecall]
232
- band_TxRx_homecall_report_times = [t for t in band_TxRx_homecall_report_times if (time.time() - self.t0 - t) < 60*self.pskr_refresh_mins]
233
- self.band_TxRx_homecall_report_times.data[band_TxRx_homecall] = band_TxRx_homecall_report_times
229
+ for band_TxRx_homecall in self.band_TxRx_homecall_recent_l4grid.data:
230
+ band_TxRx_homecall_recent_l4grid = self.band_TxRx_homecall_recent_l4grid.data[band_TxRx_homecall]
231
+ band_TxRx_homecall_recent_l4grid = [t for t in band_TxRx_homecall_recent_l4grid if (time.time() - t) < 60*self.pskr_refresh_mins]
232
+ self.band_TxRx_homecall_recent_l4grid.data[band_TxRx_homecall] = band_TxRx_homecall_recent_l4grid
234
233
 
235
234
  # count number of local Tx and Rx, and identify the local Tx and Rx with most remote spots
236
- for band_TxRx_homecall in self.band_TxRx_homecall_report_times.data:
237
- band_TxRx_homecall_report_times = self.band_TxRx_homecall_report_times.data[band_TxRx_homecall]
238
- if len(band_TxRx_homecall_report_times):
235
+ for band_TxRx_homecall in self.band_TxRx_homecall_recent_l4grid.data:
236
+ band_TxRx_homecall_recent_l4grid = self.band_TxRx_homecall_recent_l4grid.data[band_TxRx_homecall]
237
+ if len(band_TxRx_homecall_recent_l4grid):
239
238
  b, iTxRx, c = band_TxRx_homecall.split('_')
240
239
  iTxRx = int(iTxRx)
241
240
  self.home_activity.setdefault(b, [0, 0])
242
241
  self.home_activity[b][iTxRx] +=1
243
242
  self.home_most_remotes.setdefault(b, [('Nobody',0), ('Nobody',0)])
244
- nremotes = len(band_TxRx_homecall_report_times)
243
+ nremotes = len(band_TxRx_homecall_recent_l4grid)
245
244
  current_winner = self.home_most_remotes[b][iTxRx]
246
245
  if nremotes > current_winner[1]:
247
246
  if c != self.my_call:
248
247
  self.home_most_remotes[b][iTxRx] = (c, nremotes)
249
248
 
250
249
  def get_spot_counts(self, band, call):
251
- tx_reports = self.band_TxRx_homecall_report_times.data.get(f"{band}_0_{call}", [])
252
- rx_reports = self.band_TxRx_homecall_report_times.data.get(f"{band}_1_{call}", [])
250
+ tx_reports = self.band_TxRx_homecall_recent_l4grid.data.get(f"{band}_0_{call}", [])
251
+ rx_reports = self.band_TxRx_homecall_recent_l4grid.data.get(f"{band}_1_{call}", [])
253
252
  n_spotting = len(tx_reports) if tx_reports else 0
254
253
  n_spotted = len(rx_reports) if rx_reports else 0
255
254
  return n_spotted, n_spotting
@@ -9,7 +9,8 @@ class PSKR_MQTT_listener:
9
9
  def __init__(self, home_square, on_spot):
10
10
  self.home_square = home_square
11
11
  self.on_spot = on_spot
12
- mqttc = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2)
12
+ client_id = "PyFT8_" + ''.join(random.choice('0123456789ABCDEF') for i in range(16))
13
+ mqttc = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2, client_id)
13
14
  mqttc.on_connect = self.on_connect
14
15
  mqttc.on_message = self.on_message
15
16
  try:
@@ -9,13 +9,13 @@ from PyFT8.pskreporter import PSKR_upload
9
9
  from PyFT8.gui import Gui
10
10
  from PyFT8.transmitter import AudioOut
11
11
  from PyFT8.time_utils import global_time_utils
12
- from PyFT8.rigctrl import Rig_CAT, Rig_hamlib
12
+ from PyFT8.rigctrl import Rig_hamlib
13
13
  from PyFT8.databases import History, ADIF
14
14
 
15
- VER = '2.12.3'
15
+ VER = '3.0.2'
16
16
 
17
17
  MAX_TX_START_SECONDS = 2.5
18
- HEARING_PANEL_LIFE_MINS = 5
18
+ HEARING_PANEL_LIFE_MINS = 20
19
19
  PSKR_REFRESH_MINS = 20
20
20
  config_folder, rig, gui, qso, adif_logging, history, pskr_upload, output_device_idx = None, None, None, None, None, None, None, None
21
21
  busy_profile, hearing_me = None, None
@@ -38,9 +38,6 @@ def get_config():
38
38
  config['bands'] = {'20m':14.074}
39
39
  config['gui'] = {'loc':'km_deg', 'wb':'Y'}
40
40
  config['hamlib_rig'] = {'rigctld':'C:/WSJT/wsjtx/bin/rigctld-wsjtx', 'port': 'COM4', 'baud_rate':9600, 'model':3070}
41
- config['rig'] = {'port': 'COM4', 'baud_rate':9600,
42
- 'set_freq_command':'FEFE88E0.05.0000000000.FD', 'set_freq_value':'5|5|vfBcdLU|1|0',
43
- 'ptt_on_command':'FEFE88E0.1C00.01.FD', 'ptt_off_command':'FEFE88E0.1C00.00.FD'}
44
41
  config['pskreporter'] = {'upload':'Y'}
45
42
  with open(ini_file, 'w') as f:
46
43
  config.write(f)
@@ -351,9 +348,6 @@ def cli():
351
348
  if config.has_section('hamlib_rig'):
352
349
  console_print("Connecting to rig via Hamlib")
353
350
  rig = Rig_hamlib(config)
354
- else:
355
- console_print("Connecting to rig via CAT")
356
- rig = Rig_CAT(config)
357
351
 
358
352
  if args.outputcard_keywords:
359
353
  outputcard_keywords = args.outputcard_keywords.replace(' ','').split(',')
@@ -0,0 +1,43 @@
1
+ import time, socket, subprocess, threading
2
+
3
+ class Rig_hamlib:
4
+ def __init__(self, config):
5
+ com = config['hamlib_rig']['port']
6
+ s = config['hamlib_rig']['baud_rate']
7
+ rigctld = config['hamlib_rig']['rigctld']
8
+ rig = config['hamlib_rig']['model']
9
+ host, port ="localhost", 4532
10
+ cmd = f"{rigctld} -m {rig} -r {com} -s {s}"
11
+ self.sock = None
12
+ if not self.create_socket(host, port):
13
+ threading.Thread(target = subprocess.run, args = (cmd,)).start()
14
+ time.sleep(0.5)
15
+ if not self.create_socket(host, port):
16
+ print(f"Couldn't create socket for hamlib on {host}:{port}")
17
+ self.set_mode("PKTUSB")
18
+
19
+ def create_socket(self, host, port):
20
+ try:
21
+ self.sock = socket.create_connection((host, port))
22
+ except:
23
+ return False
24
+ return True
25
+
26
+ def cmd(self, command):
27
+ if self.sock:
28
+ self.sock.sendall((command + "\n").encode())
29
+ return self.sock.recv(1024).decode()
30
+
31
+ def set_mode(self, mode):
32
+ self.cmd(f"M {mode} 0")
33
+
34
+ def set_freq_Hz(self, hz):
35
+ self.cmd(f"F {hz}")
36
+
37
+ def ptt_on(self):
38
+ self.cmd(f"T 1")
39
+
40
+ def ptt_off(self):
41
+ self.cmd(f"T 0")
42
+
43
+
@@ -2,7 +2,7 @@ import numpy as np
2
2
  import wave
3
3
  import pyaudio
4
4
  import time
5
- from PyFT8.databases import add_call_hashes
5
+ from PyFT8.databases import hashes_for_calls, add_call_hashes
6
6
 
7
7
  #==================== AUDIO OUT ================================================================
8
8
 
@@ -56,35 +56,42 @@ class AudioOut:
56
56
  stream.stop_stream()
57
57
  stream.close()
58
58
 
59
- #==================== PACK AND ENCODE ================================================================
60
- generator_matrix_rows = ["8329ce11bf31eaf509f27fc", "761c264e25c259335493132", "dc265902fb277c6410a1bdc", "1b3f417858cd2dd33ec7f62", "09fda4fee04195fd034783a", "077cccc11b8873ed5c3d48a", "29b62afe3ca036f4fe1a9da", "6054faf5f35d96d3b0c8c3e", "e20798e4310eed27884ae90", "775c9c08e80e26ddae56318", "b0b811028c2bf997213487c", "18a0c9231fc60adf5c5ea32", "76471e8302a0721e01b12b8", "ffbccb80ca8341fafb47b2e", "66a72a158f9325a2bf67170", "c4243689fe85b1c51363a18", "0dff739414d1a1b34b1c270", "15b48830636c8b99894972e", "29a89c0d3de81d665489b0e", "4f126f37fa51cbe61bd6b94", "99c47239d0d97d3c84e0940", "1919b75119765621bb4f1e8", "09db12d731faee0b86df6b8", "488fc33df43fbdeea4eafb4", "827423ee40b675f756eb5fe", "abe197c484cb74757144a9a", "2b500e4bc0ec5a6d2bdbdd0", "c474aa53d70218761669360", "8eba1a13db3390bd6718cec", "753844673a27782cc42012e", "06ff83a145c37035a5c1268", "3b37417858cc2dd33ec3f62", "9a4a5a28ee17ca9c324842c", "bc29f465309c977e89610a4", "2663ae6ddf8b5ce2bb29488", "46f231efe457034c1814418", "3fb2ce85abe9b0c72e06fbe", "de87481f282c153971a0a2e", "fcd7ccf23c69fa99bba1412", "f0261447e9490ca8e474cec", "4410115818196f95cdd7012", "088fc31df4bfbde2a4eafb4", "b8fef1b6307729fb0a078c0", "5afea7acccb77bbc9d99a90", "49a7016ac653f65ecdc9076", "1944d085be4e7da8d6cc7d0", "251f62adc4032f0ee714002", "56471f8702a0721e00b12b8", "2b8e4923f2dd51e2d537fa0", "6b550a40a66f4755de95c26", "a18ad28d4e27fe92a4f6c84", "10c2e586388cb82a3d80758", "ef34a41817ee02133db2eb0", "7e9c0c54325a9c15836e000", "3693e572d1fde4cdf079e86", "bfb2cec5abe1b0c72e07fbe", "7ee18230c583cccc57d4b08", "a066cb2fedafc9f52664126", "bb23725abc47cc5f4cc4cd2", "ded9dba3bee40c59b5609b4", "d9a7016ac653e6decdc9036", "9ad46aed5f707f280ab5fc4", "e5921c77822587316d7d3c2", "4f14da8242a8b86dca73352", "8b8b507ad467d4441df770e", "22831c9cf1169467ad04b68", "213b838fe2ae54c38ee7180", "5d926b6dd71f085181a4e12", "66ab79d4b29ee6e69509e56", "958148682d748a38dd68baa", "b8ce020cf069c32a723ab14", "f4331d6d461607e95752746", "6da23ba424b9596133cf9c8", "a636bcbc7b30c5fbeae67fe", "5cb0d86a07df654a9089a20", "f11f106848780fc9ecdd80a", "1fbb5364fb8d2c9d730d5ba", "fcb86bc70a50c9d02a5d034", "a534433029eac15f322e34c", "c989d9c7c3d3b8c55d75130", "7bb38b2f0186d46643ae962", "2644ebadeb44b9467d1f42c", "608cc857594bfbb55d69600"]
61
- kGEN = np.array([int(row,16)>>1 for row in generator_matrix_rows])
59
+ #==================== PACK ================================================================
60
+
61
+ def ifindex(arr, val, default = None):
62
+ return arr.index(val) if val in arr else default
62
63
 
63
64
  def pack_message(c1, c2, gr):
64
65
  symbols, bits77 = _pack_message(c1, c2, gr)
65
66
  return symbols
66
67
 
67
68
  def _pack_message(c1, c2, gr):
68
- c28a, p1a = pack_ft8_c28(c1)
69
- c28b, p1b = pack_ft8_c28(c2)
69
+ c29a, c29b = pack_ft8_c29(c1), pack_ft8_c29(c2)
70
70
  g15, ir = pack_ft8_g15(gr)
71
- i3 = 2 if c1.endswith('/P') or c2.endswith('/P') else 1
72
- n3 = 0
73
- if(c28a>=0 and c28b>=0):
71
+ if c29a and c29b:
72
+ c28a, p1a = c29a
73
+ c28b, p1b = c29b
74
+ i3 = 2 if c1.endswith('/P') or c2.endswith('/P') else 1
75
+ n3 = 0
76
+ 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)
77
+ symbols = encode_bits77(bits77)
78
+ if c29b and not c29a:
79
+ add_call_hashes(c1)
80
+ c28a, p1a = 2063592 + hashes_for_calls[c1][2][0], 0
81
+ c28b, p1b = c29b
82
+ i3 = 2 if c2.endswith('/P') else 1
83
+ n3 = 0
74
84
  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
85
  symbols = encode_bits77(bits77)
76
- else:
77
- i3 = 4
78
- full_call = c1 if c28b>0 else c2
79
- hash_call = c2 if c28b>0 else c1
80
- c58 = pack_ft8_c58(full_call)
81
- hashes = add_call_hashes(hash_call)
82
- swp = 1 if hash_call == c2 else 0
83
- rrr, cq_ = 0, 0
84
- if gr in ('RRR', 'RR73', '73'):
85
- rrr = ('', 'RRR', 'RR73', '73').index(gr)
86
- bits77 = (hashes[1]<<58+1+2+1+3) | (c58 <<1+2+1+3) | (swp<<2+1+3) | (rrr<<1+3) | (cq_<<3) | (i3)
86
+ if c29a and not c29b:
87
+ add_call_hashes(c2)
88
+ c28b, p1b = 2063592 + hashes_for_calls[c2][2][0], 0
89
+ c28a, p1a = c29a
90
+ i3 = 2 if c1.endswith('/P') else 1
91
+ n3 = 0
92
+ 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)
87
93
  symbols = encode_bits77(bits77)
94
+
88
95
  return symbols, bits77
89
96
 
90
97
 
@@ -96,24 +103,23 @@ def pack_ft8_c58(call):
96
103
  n58 = n58*38 + chars.index(call[i])
97
104
  return n58
98
105
 
99
- def pack_ft8_c28(call):
100
- if '/' in call and not call.endswith("P") and not call.endswith("R"): return -1, 0
101
- tkns = ['DE','QRZ','CQ']
102
- if (call in tkns):
103
- c28, p1 = tkns.index(call), 0
104
- else:
106
+ def pack_ft8_c29(call):
107
+ if '/' not in call or call.endswith("P") or call.endswith("R"):
108
+ t = ifindex(['DE','QRZ','CQ'], call)
109
+ if t is not None:
110
+ return t, 0
105
111
  p1 = 1 if call[-2:] in ('/P', '/R') else 0
106
112
  call = call.replace('/P','').replace('/R','')
107
- if len(call) > 6:
108
- return -1, 0
109
- prepend_space = '' if call[2].isdigit() else ' '
110
- call = (prepend_space + call + ' ')[:6]
111
- a = ' 0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ'
112
- b = ' ABCDEFGHIJKLMNOPQRSTUVWXYZ'
113
- x = [a.index(call[0]), a.index(call[1])-1, a.index(call[2])-1, b.index(call[3]), b.index(call[4]), b.index(call[5]), 1 ]
114
- y = [36*10*27*27*27, 10*27*27*27, 27*27*27, 27*27, 27, 1, 2_063_592 + 4_194_304]
115
- c28 = int(np.dot(x,y))
116
- return c28, p1
113
+ if len(call) <= 6:
114
+ prepend_space = '' if call[2].isdigit() else ' '
115
+ call = (prepend_space + call + ' ')[:6]
116
+ a = ' 0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ'
117
+ b = ' ABCDEFGHIJKLMNOPQRSTUVWXYZ'
118
+ x = [ifindex(a, call[0]), ifindex(a[1:], call[1]), ifindex(a[1:], call[2]), ifindex(b, call[3]), ifindex(b, call[4]), ifindex(b, call[5]), 1 ]
119
+ if not None in x:
120
+ y = [36*10*27*27*27, 10*27*27*27, 27*27*27, 27*27, 27, 1, 2_063_592 + 4_194_304]
121
+ c28 = int(np.dot(x,y))
122
+ return c28, p1
117
123
 
118
124
  def pack_ft8_g15(txt):
119
125
  ir = 0
@@ -138,6 +144,11 @@ def pack_ft8_g15(txt):
138
144
  v = v * 10 + int(txt[3])
139
145
  return int(v), ir
140
146
 
147
+ #============ ENCODE ========================================================================================================================
148
+
149
+ generator_matrix_rows = ["8329ce11bf31eaf509f27fc", "761c264e25c259335493132", "dc265902fb277c6410a1bdc", "1b3f417858cd2dd33ec7f62", "09fda4fee04195fd034783a", "077cccc11b8873ed5c3d48a", "29b62afe3ca036f4fe1a9da", "6054faf5f35d96d3b0c8c3e", "e20798e4310eed27884ae90", "775c9c08e80e26ddae56318", "b0b811028c2bf997213487c", "18a0c9231fc60adf5c5ea32", "76471e8302a0721e01b12b8", "ffbccb80ca8341fafb47b2e", "66a72a158f9325a2bf67170", "c4243689fe85b1c51363a18", "0dff739414d1a1b34b1c270", "15b48830636c8b99894972e", "29a89c0d3de81d665489b0e", "4f126f37fa51cbe61bd6b94", "99c47239d0d97d3c84e0940", "1919b75119765621bb4f1e8", "09db12d731faee0b86df6b8", "488fc33df43fbdeea4eafb4", "827423ee40b675f756eb5fe", "abe197c484cb74757144a9a", "2b500e4bc0ec5a6d2bdbdd0", "c474aa53d70218761669360", "8eba1a13db3390bd6718cec", "753844673a27782cc42012e", "06ff83a145c37035a5c1268", "3b37417858cc2dd33ec3f62", "9a4a5a28ee17ca9c324842c", "bc29f465309c977e89610a4", "2663ae6ddf8b5ce2bb29488", "46f231efe457034c1814418", "3fb2ce85abe9b0c72e06fbe", "de87481f282c153971a0a2e", "fcd7ccf23c69fa99bba1412", "f0261447e9490ca8e474cec", "4410115818196f95cdd7012", "088fc31df4bfbde2a4eafb4", "b8fef1b6307729fb0a078c0", "5afea7acccb77bbc9d99a90", "49a7016ac653f65ecdc9076", "1944d085be4e7da8d6cc7d0", "251f62adc4032f0ee714002", "56471f8702a0721e00b12b8", "2b8e4923f2dd51e2d537fa0", "6b550a40a66f4755de95c26", "a18ad28d4e27fe92a4f6c84", "10c2e586388cb82a3d80758", "ef34a41817ee02133db2eb0", "7e9c0c54325a9c15836e000", "3693e572d1fde4cdf079e86", "bfb2cec5abe1b0c72e07fbe", "7ee18230c583cccc57d4b08", "a066cb2fedafc9f52664126", "bb23725abc47cc5f4cc4cd2", "ded9dba3bee40c59b5609b4", "d9a7016ac653e6decdc9036", "9ad46aed5f707f280ab5fc4", "e5921c77822587316d7d3c2", "4f14da8242a8b86dca73352", "8b8b507ad467d4441df770e", "22831c9cf1169467ad04b68", "213b838fe2ae54c38ee7180", "5d926b6dd71f085181a4e12", "66ab79d4b29ee6e69509e56", "958148682d748a38dd68baa", "b8ce020cf069c32a723ab14", "f4331d6d461607e95752746", "6da23ba424b9596133cf9c8", "a636bcbc7b30c5fbeae67fe", "5cb0d86a07df654a9089a20", "f11f106848780fc9ecdd80a", "1fbb5364fb8d2c9d730d5ba", "fcb86bc70a50c9d02a5d034", "a534433029eac15f322e34c", "c989d9c7c3d3b8c55d75130", "7bb38b2f0186d46643ae962", "2644ebadeb44b9467d1f42c", "608cc857594bfbb55d69600"]
150
+ kGEN = np.array([int(row,16)>>1 for row in generator_matrix_rows])
151
+
141
152
  def ldpc_encode(msg_crc: int) -> int:
142
153
  msg_crc = int(msg_crc)
143
154
  parity_bits = 0
@@ -182,6 +193,8 @@ def append_crc(bits77_int):
182
193
  #==================== TESTS ================================================================
183
194
 
184
195
  if __name__ == "__main__":
196
+
197
+ print("Test standard calls")
185
198
  OK = True
186
199
  msgs = [("G1OJS/P", "G1OJS/P", "IO90"),("WM3PEN","EA6VQ","+08"),("E67A/P","EA6VQ","R-08"),
187
200
  ("CQ","CT7ARQ/P","JO03"), ("EC5A","9A5E","RR73"), ("EC5A/P","9A5E","73"), ("EC5A/MM","9A5E","73"),
@@ -195,5 +208,24 @@ if __name__ == "__main__":
195
208
  #print(''.join([str(s) for s in symbols]))
196
209
  print("\nPASSED" if OK else "\nFAILED")
197
210
 
211
+ print("\nTest non-standard calls")
212
+ OK = True
213
+ msgs = [("SX200M","G1OJS","IO90"),
214
+ ("G1OJS","SX200M","CB21"),
215
+ ("CQ","SX200M","CB21")]
216
+ expected_symbols = ["3140652203407700750145313654745000003140652276403052651370607066332604363140652",
217
+ "",
218
+ "3140652564261623472565070174400214333140652601351750040163007617513443213140652"]
219
+ for i, msg_tx in enumerate(msgs):
220
+ symbols, bits77 = _pack_message(*msg_tx)
221
+ from PyFT8.receiver import unpack
222
+ msg_rx = unpack(bits77)
223
+ print(f"\n{msg_tx}\n{msg_rx}")
224
+ OK = OK and (msg_tx == msg_rx) or 'implemented' in msg_rx
225
+ print(f"exp: {expected_symbols[i]}")
226
+ print(f"gen: {''.join([str(s) for s in symbols])}")
227
+ print("\nPASSED" if OK else "\nFAILED")
228
+
229
+ print("\nUnpack 'CQ DX G1OJS IO90'")
198
230
  print(unpack(int('00000000000000000100011011110000010010000000000111000001100011111000010010001',2)))
199
231
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: PyFT8
3
- Version: 2.12.3
3
+ Version: 3.0.2
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
@@ -27,7 +27,8 @@ PyFT8 is somewhat experimental, with a focus on demonstrating FT8 written in Pyt
27
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
28
 
29
29
  ## Features
30
- - Rx and Tx of standard messages with optional /P and /R, and nonstandard calls plus hashed calls
30
+ - Rx and Tx of standard messages with optional /P and /R
31
+ - RX of nonstandard calls plus hashed calls
31
32
  - Launches quickly (~2 seconds on my old Dell Optiplex 790)
32
33
  - Use with or without gui (receive and send messages via command line commands)
33
34
  - Automatically chooses clearest Tx frequency
@@ -35,14 +36,13 @@ If you're interested in how this works, maybe have a look at [MiniPyFT8](https:/
35
36
  - Finds sound cards by keywords so follows them if windows moves them ...
36
37
  - Logs QSOs to ADIF file and all spots to WSJTX-style ALL.txt file
37
38
  - Uploads spots to pskreporter
38
- - Direct CAT control for some rigs, designed to drop connection when not used, allowing sharing of rig's serial port
39
- - Or control rigs via Hamlib
39
+ - Control rigs via Hamlib
40
40
 
41
41
  The Gui shows:
42
42
  - Simultaneous views of odd and even cycles
43
43
  - Messages overlaid on waterfall signals that produce them
44
44
  - Worked-before info and fine grid locators / distance and bearing in the message boxes
45
- - List of stations hearing your transmissions on the selected band
45
+ - List of stations hearing your transmissions on the selected band, alternating with list of stations you're hearing. On these lists, 'new' callsigns (first heard since program launch) are in white font.
46
46
  - Marker on message if 2nd call is hearing you
47
47
  - Band activity in your level 4 square live updated next to band select buttons
48
48
  - Number of remote stations hearing your Tx, number of remote Txs that you're hearing, plus the same info for the 'best' station in your level 4 square
@@ -63,7 +63,7 @@ If you want to install this software without getting into the code, you can inst
63
63
  pip install PyFT8
64
64
  ```
65
65
 
66
- Once installed, you can use the following commands to run it. Otherwise, please download or browse the code, or fork the repo and play with it! If you do fork it, please check back here as I'm constantly (as of Jan 2026) rewriting and improving.
66
+ Once installed, you can use the following commands to run it. Otherwise, please download or browse the code, or fork the repo and play with it! If you do fork it, please check back here as I'm constantly (as of April 2026) rewriting and improving.
67
67
 
68
68
  The use cases below are the ones held in the test cases at the bottom of the main pyft8.py file. Where a config folder and .ini file is required, if you leave off the '-c {folder}' option, you'll be asked if you want to create a .ini file in the current directory.
69
69
 
@@ -77,30 +77,19 @@ The use cases below are the ones held in the test cases at the bottom of the mai
77
77
  | Command line create a wav file | pyft8 -w "Mywav.wav" -m "CQ G1OJS IO90"| No config folder / .ini file required |
78
78
 
79
79
  ### Rig control
80
- I've included the Python code that I use with my Icom IC-7100 in the file 'rigctrl.py', and believe I've moved sufficient 'specification' for the rig protocol into the .ini file so that you can paste in your own rig specification (see for e.g. the Omnirig .ini file for your rig) and get it working with PyFT8 controlling PTT and frequency. I designed this code to drop the serial connection when it's not required, so that the rig's serial port can be accessed by other software at the same time (assuming that the other software returns the favour and doesn't permanently hog the serial port).
81
-
82
- I've also included a basic Hamlib interface which launches rigctld and uses that to control the rig. To use this, make sure that the hamlib section of the ini file is populated; this will then take precedence over the direct CAT control section.
83
-
84
- Alternatively, you can run PyFT8 without rig control; if there is no rig found, PyFT8 defaults to running without a rig connected. In this case, you need to provide your own PTT method and note that the band buttons will only set the information used for logging QSOs to the PyFT8.adi file. Or you can use PyFT8 as Rx-only.
80
+ I designed PyFT8 to be minimal as far as possible, and it needs very limited control of the rig in order to transmit. However, whilst previous versions used direct CAT control and Hamlib as a fallback, Version 3.0.0 moves to Hamlib as the only interface.
85
81
 
86
82
  ## Customising
87
- Even if you don't want to edit the code, there's quite a bit that you can change in the .ini file. I haven't written a user guide yet, so here's my own .ini file for reference. I use CAT control so my hamlib block is disabled by adding a _ to the name of the block (I keep it there for testing only). I need to add some more switches really to catch up with recent additions.
83
+ Even if you don't want to edit the code, there's quite a bit that you can change in the .ini file. I haven't written a user guide yet, so here's my own .ini file for reference. You'll note that my instance of rigctld is in C:/WSJT/wsjtx/bin/rigctld-wsjtx, but any rigctld should also work. PyFT8 launches this if it's not already running (to be precise, it launches it if it can't open a socket on localhost:4532).
88
84
 
89
85
  ```
90
86
  [station]
91
87
  call = G1OJS
92
- grid = IO90******
88
+ grid = IO******
93
89
  [gui]
94
90
  loc = km_deg
95
91
  wb = Y
96
- [rig]
97
- port = COM4
98
- baud_rate = 9600
99
- set_freq_command = FEFE88E0.05.0000000000.FD
100
- set_freq_value = 5|5|vfBcdLU|1|0
101
- ptt_on_command = FEFE88E0.1C00.01.FD
102
- ptt_off_command = FEFE88E0.1C00.00.FD
103
- [_hamlib_rig]
92
+ [hamlib_rig]
104
93
  rigctld = C:/WSJT/wsjtx/bin/rigctld-wsjtx
105
94
  port = COM4
106
95
  baud_rate = 9600
@@ -139,7 +128,7 @@ PyFT8 doesn't decode / encode *all* message types. The table below shows which a
139
128
  |1|Std Msg |Y| Y |Standard <=6 char callsigns, can include /R |
140
129
  |2|EU VHF |Y|Y| Standard <=6 char callsigns, can include /P |
141
130
  |3|RTTY RU | | | |
142
- |4|NonStd Call |Y|Y| <=11 char callsigns + hashed call|
131
+ |4|NonStd Call |Y|N| <=11 char callsigns + hashed call|
143
132
  |5|EU VHF | | | |
144
133
 
145
134
  ## Acknowledgements
@@ -8,7 +8,8 @@ PyFT8 is somewhat experimental, with a focus on demonstrating FT8 written in Pyt
8
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
9
 
10
10
  ## Features
11
- - Rx and Tx of standard messages with optional /P and /R, and nonstandard calls plus hashed calls
11
+ - Rx and Tx of standard messages with optional /P and /R
12
+ - RX of nonstandard calls plus hashed calls
12
13
  - Launches quickly (~2 seconds on my old Dell Optiplex 790)
13
14
  - Use with or without gui (receive and send messages via command line commands)
14
15
  - Automatically chooses clearest Tx frequency
@@ -16,14 +17,13 @@ If you're interested in how this works, maybe have a look at [MiniPyFT8](https:/
16
17
  - Finds sound cards by keywords so follows them if windows moves them ...
17
18
  - Logs QSOs to ADIF file and all spots to WSJTX-style ALL.txt file
18
19
  - Uploads spots to pskreporter
19
- - Direct CAT control for some rigs, designed to drop connection when not used, allowing sharing of rig's serial port
20
- - Or control rigs via Hamlib
20
+ - Control rigs via Hamlib
21
21
 
22
22
  The Gui shows:
23
23
  - Simultaneous views of odd and even cycles
24
24
  - Messages overlaid on waterfall signals that produce them
25
25
  - Worked-before info and fine grid locators / distance and bearing in the message boxes
26
- - List of stations hearing your transmissions on the selected band
26
+ - List of stations hearing your transmissions on the selected band, alternating with list of stations you're hearing. On these lists, 'new' callsigns (first heard since program launch) are in white font.
27
27
  - Marker on message if 2nd call is hearing you
28
28
  - Band activity in your level 4 square live updated next to band select buttons
29
29
  - Number of remote stations hearing your Tx, number of remote Txs that you're hearing, plus the same info for the 'best' station in your level 4 square
@@ -44,7 +44,7 @@ If you want to install this software without getting into the code, you can inst
44
44
  pip install PyFT8
45
45
  ```
46
46
 
47
- Once installed, you can use the following commands to run it. Otherwise, please download or browse the code, or fork the repo and play with it! If you do fork it, please check back here as I'm constantly (as of Jan 2026) rewriting and improving.
47
+ Once installed, you can use the following commands to run it. Otherwise, please download or browse the code, or fork the repo and play with it! If you do fork it, please check back here as I'm constantly (as of April 2026) rewriting and improving.
48
48
 
49
49
  The use cases below are the ones held in the test cases at the bottom of the main pyft8.py file. Where a config folder and .ini file is required, if you leave off the '-c {folder}' option, you'll be asked if you want to create a .ini file in the current directory.
50
50
 
@@ -58,30 +58,19 @@ The use cases below are the ones held in the test cases at the bottom of the mai
58
58
  | Command line create a wav file | pyft8 -w "Mywav.wav" -m "CQ G1OJS IO90"| No config folder / .ini file required |
59
59
 
60
60
  ### Rig control
61
- I've included the Python code that I use with my Icom IC-7100 in the file 'rigctrl.py', and believe I've moved sufficient 'specification' for the rig protocol into the .ini file so that you can paste in your own rig specification (see for e.g. the Omnirig .ini file for your rig) and get it working with PyFT8 controlling PTT and frequency. I designed this code to drop the serial connection when it's not required, so that the rig's serial port can be accessed by other software at the same time (assuming that the other software returns the favour and doesn't permanently hog the serial port).
62
-
63
- I've also included a basic Hamlib interface which launches rigctld and uses that to control the rig. To use this, make sure that the hamlib section of the ini file is populated; this will then take precedence over the direct CAT control section.
64
-
65
- Alternatively, you can run PyFT8 without rig control; if there is no rig found, PyFT8 defaults to running without a rig connected. In this case, you need to provide your own PTT method and note that the band buttons will only set the information used for logging QSOs to the PyFT8.adi file. Or you can use PyFT8 as Rx-only.
61
+ I designed PyFT8 to be minimal as far as possible, and it needs very limited control of the rig in order to transmit. However, whilst previous versions used direct CAT control and Hamlib as a fallback, Version 3.0.0 moves to Hamlib as the only interface.
66
62
 
67
63
  ## Customising
68
- Even if you don't want to edit the code, there's quite a bit that you can change in the .ini file. I haven't written a user guide yet, so here's my own .ini file for reference. I use CAT control so my hamlib block is disabled by adding a _ to the name of the block (I keep it there for testing only). I need to add some more switches really to catch up with recent additions.
64
+ Even if you don't want to edit the code, there's quite a bit that you can change in the .ini file. I haven't written a user guide yet, so here's my own .ini file for reference. You'll note that my instance of rigctld is in C:/WSJT/wsjtx/bin/rigctld-wsjtx, but any rigctld should also work. PyFT8 launches this if it's not already running (to be precise, it launches it if it can't open a socket on localhost:4532).
69
65
 
70
66
  ```
71
67
  [station]
72
68
  call = G1OJS
73
- grid = IO90******
69
+ grid = IO******
74
70
  [gui]
75
71
  loc = km_deg
76
72
  wb = Y
77
- [rig]
78
- port = COM4
79
- baud_rate = 9600
80
- set_freq_command = FEFE88E0.05.0000000000.FD
81
- set_freq_value = 5|5|vfBcdLU|1|0
82
- ptt_on_command = FEFE88E0.1C00.01.FD
83
- ptt_off_command = FEFE88E0.1C00.00.FD
84
- [_hamlib_rig]
73
+ [hamlib_rig]
85
74
  rigctld = C:/WSJT/wsjtx/bin/rigctld-wsjtx
86
75
  port = COM4
87
76
  baud_rate = 9600
@@ -120,7 +109,7 @@ PyFT8 doesn't decode / encode *all* message types. The table below shows which a
120
109
  |1|Std Msg |Y| Y |Standard <=6 char callsigns, can include /R |
121
110
  |2|EU VHF |Y|Y| Standard <=6 char callsigns, can include /P |
122
111
  |3|RTTY RU | | | |
123
- |4|NonStd Call |Y|Y| <=11 char callsigns + hashed call|
112
+ |4|NonStd Call |Y|N| <=11 char callsigns + hashed call|
124
113
  |5|EU VHF | | | |
125
114
 
126
115
  ## Acknowledgements
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "PyFT8"
3
- version = "2.12.3"
3
+ version = "3.0.2"
4
4
  license = "GPL-3.0-or-later"
5
5
 
6
6
  authors = [
@@ -1,102 +0,0 @@
1
- import serial, time, socket, subprocess, threading
2
-
3
- class Rig_CAT:
4
- def __init__(self, config, verbose = False):
5
- self.serial_port = False
6
- self.port = config['rig']['port']
7
- self.baud_rate = config['rig']['baud_rate']
8
- self.ptt_on_cmd = self.parse_configstr(config['rig']['ptt_on_command']) if config['rig']['ptt_on_command']else None
9
- self.ptt_off_cmd = self.parse_configstr(config['rig']['ptt_off_command']) if config['rig']['ptt_off_command'] else None
10
- self.set_freq_cmd = self.parse_configstr(config['rig']['set_freq_command']) if config['rig']['set_freq_command'] else None
11
- self.set_freq_value = config['rig']['set_freq_value'] if config['rig']['set_freq_value'] else None
12
- self.verbose = verbose
13
-
14
- def parse_configstr(self, configstr):
15
- if "." in configstr:
16
- hexstr = configstr.replace(".", "")
17
- return bytearray.fromhex(hexstr)
18
- else:
19
- return bytearray(configstr.encode())
20
-
21
- def vprint(self, text):
22
- if self.verbose:
23
- print(text)
24
-
25
- def _sendCAT(self, msg):
26
- try:
27
- self.serial_port = serial.Serial(port = self.port, baudrate = self.baud_rate, timeout = 0.1)
28
- except Exception as e:
29
- print(f"[CAT] couldn't open {self.port}: {e}")
30
- if (self.serial_port):
31
- self.serial_port.reset_input_buffer()
32
- self.vprint(f"[CAT] send {msg.hex(' ')}")
33
- try:
34
- self.serial_port.write(msg)
35
- time.sleep(0.05)
36
- self.serial_port.close()
37
- except Exception as e:
38
- print(f"[CAT] couldn't send CAT command {msg} on {self.port}: {e}")
39
-
40
- def set_freq_Hz(self, freqHz):
41
- if self.set_freq_cmd and self.set_freq_value:
42
- self.vprint(f"[CAT] SET frequency to {freqHz} Hz")
43
- start, length, fmt, mult, offset = self.set_freq_value.split("|")
44
- start, length, mult, offset = int(start), int(length), int(mult), int(offset)
45
- fVal = freqHz * mult + offset
46
- nDigits = length if fmt == "text" else 2*length
47
- s = f"{fVal:0{nDigits}d}"
48
- if fmt=='text':
49
- fBytes = s.encode()
50
- else:
51
- pairs = [(int(s[i]) << 4) | int(s[i+1]) for i in range(0, len(s), 2)]
52
- if fmt == "vfBcdLU":
53
- fBytes = bytes(pairs[::-1])
54
- else:
55
- fBytes = bytes(pairs)
56
- cmd = bytearray(self.set_freq_cmd)
57
- cmd[start:start+length] = fBytes
58
- if fmt.startswith("vfBcd"): # CI-V
59
- cmd = b'\x00' + cmd
60
- self._sendCAT(cmd)
61
-
62
- def ptt_on(self):
63
- if self.ptt_on_cmd:
64
- self.vprint(f"[CAT] PTT On")
65
- self._sendCAT(self.ptt_on_cmd)
66
-
67
- def ptt_off(self):
68
- if self.ptt_off_cmd:
69
- self.vprint(f"[CAT] PTT Off")
70
- self._sendCAT(self.ptt_off_cmd)
71
-
72
- class Rig_hamlib:
73
- def __init__(self, config):
74
- com = config['hamlib_rig']['port']
75
- s = config['hamlib_rig']['baud_rate']
76
- rigctld = config['hamlib_rig']['rigctld']
77
- rig = config['hamlib_rig']['model']
78
- host, port ="localhost", 4532
79
- cmd = f"{rigctld} -m {rig} -r /{com} -s {s}"
80
- threading.Thread(target = subprocess.run, args = (cmd,)).start()
81
- self.sock = socket.create_connection((host, port))
82
-
83
- def cmd(self, command):
84
- self.sock.sendall((command + "\n").encode())
85
- return self.sock.recv(1024).decode()
86
-
87
- def set_freq_Hz(self, hz):
88
- self.cmd(f"F {hz}")
89
-
90
- def ptt_on(self):
91
- self.cmd(f"T 1")
92
-
93
- def ptt_off(self):
94
- self.cmd(f"T 0")
95
-
96
-
97
- if __name__ == '__main__':
98
- rig = Rig_hamlib()
99
- rig.set_freq_Hz(14074000)
100
- rig.ptt_on()
101
- time.sleep(0.1)
102
- rig.ptt_off()
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