PyFT8 2.12.3__tar.gz → 3.0.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 (30) hide show
  1. {pyft8-2.12.3 → pyft8-3.0.0}/PKG-INFO +8 -20
  2. {pyft8-2.12.3 → pyft8-3.0.0}/PyFT8/databases.py +15 -16
  3. {pyft8-2.12.3 → pyft8-3.0.0}/PyFT8/pyft8.py +3 -9
  4. pyft8-3.0.0/PyFT8/rigctrl.py +43 -0
  5. {pyft8-2.12.3 → pyft8-3.0.0}/PyFT8.egg-info/PKG-INFO +8 -20
  6. {pyft8-2.12.3 → pyft8-3.0.0}/README.md +7 -19
  7. {pyft8-2.12.3 → pyft8-3.0.0}/pyproject.toml +1 -1
  8. pyft8-2.12.3/PyFT8/rigctrl.py +0 -102
  9. {pyft8-2.12.3 → pyft8-3.0.0}/LICENSE +0 -0
  10. {pyft8-2.12.3 → pyft8-3.0.0}/MANIFEST.in +0 -0
  11. {pyft8-2.12.3 → pyft8-3.0.0}/PyFT8/__init__.py +0 -0
  12. {pyft8-2.12.3 → pyft8-3.0.0}/PyFT8/gui.py +0 -0
  13. {pyft8-2.12.3 → pyft8-3.0.0}/PyFT8/pskreporter.py +0 -0
  14. {pyft8-2.12.3 → pyft8-3.0.0}/PyFT8/receiver.py +0 -0
  15. {pyft8-2.12.3 → pyft8-3.0.0}/PyFT8/time_utils.py +0 -0
  16. {pyft8-2.12.3 → pyft8-3.0.0}/PyFT8/transmitter.py +0 -0
  17. {pyft8-2.12.3 → pyft8-3.0.0}/PyFT8.egg-info/SOURCES.txt +0 -0
  18. {pyft8-2.12.3 → pyft8-3.0.0}/PyFT8.egg-info/dependency_links.txt +0 -0
  19. {pyft8-2.12.3 → pyft8-3.0.0}/PyFT8.egg-info/entry_points.txt +0 -0
  20. {pyft8-2.12.3 → pyft8-3.0.0}/PyFT8.egg-info/requires.txt +0 -0
  21. {pyft8-2.12.3 → pyft8-3.0.0}/PyFT8.egg-info/top_level.txt +0 -0
  22. {pyft8-2.12.3 → pyft8-3.0.0}/setup.cfg +0 -0
  23. {pyft8-2.12.3 → pyft8-3.0.0}/tests/dev/CQ AAAA.py +0 -0
  24. {pyft8-2.12.3 → pyft8-3.0.0}/tests/dev/osd.py +0 -0
  25. {pyft8-2.12.3 → pyft8-3.0.0}/tests/dev/test_generate_wav.py +0 -0
  26. {pyft8-2.12.3 → pyft8-3.0.0}/tests/dev/test_loopback_performance.py +0 -0
  27. {pyft8-2.12.3 → pyft8-3.0.0}/tests/dev/view_worked_before.py +0 -0
  28. {pyft8-2.12.3 → pyft8-3.0.0}/tests/plot_baseline.py +0 -0
  29. {pyft8-2.12.3 → pyft8-3.0.0}/tests/spare.py +0 -0
  30. {pyft8-2.12.3 → pyft8-3.0.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.12.3
3
+ Version: 3.0.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
@@ -35,14 +35,13 @@ If you're interested in how this works, maybe have a look at [MiniPyFT8](https:/
35
35
  - Finds sound cards by keywords so follows them if windows moves them ...
36
36
  - Logs QSOs to ADIF file and all spots to WSJTX-style ALL.txt file
37
37
  - 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
38
+ - Control rigs via Hamlib
40
39
 
41
40
  The Gui shows:
42
41
  - Simultaneous views of odd and even cycles
43
42
  - Messages overlaid on waterfall signals that produce them
44
43
  - 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
44
+ - 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
45
  - Marker on message if 2nd call is hearing you
47
46
  - Band activity in your level 4 square live updated next to band select buttons
48
47
  - 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 +62,7 @@ If you want to install this software without getting into the code, you can inst
63
62
  pip install PyFT8
64
63
  ```
65
64
 
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.
65
+ 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
66
 
68
67
  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
68
 
@@ -77,30 +76,19 @@ The use cases below are the ones held in the test cases at the bottom of the mai
77
76
  | Command line create a wav file | pyft8 -w "Mywav.wav" -m "CQ G1OJS IO90"| No config folder / .ini file required |
78
77
 
79
78
  ### 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.
79
+ 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
80
 
86
81
  ## 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.
82
+ 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
83
 
89
84
  ```
90
85
  [station]
91
86
  call = G1OJS
92
- grid = IO90******
87
+ grid = IO******
93
88
  [gui]
94
89
  loc = km_deg
95
90
  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]
91
+ [hamlib_rig]
104
92
  rigctld = C:/WSJT/wsjtx/bin/rigctld-wsjtx
105
93
  port = COM4
106
94
  baud_rate = 9600
@@ -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,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.0'
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
+
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: PyFT8
3
- Version: 2.12.3
3
+ Version: 3.0.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
@@ -35,14 +35,13 @@ If you're interested in how this works, maybe have a look at [MiniPyFT8](https:/
35
35
  - Finds sound cards by keywords so follows them if windows moves them ...
36
36
  - Logs QSOs to ADIF file and all spots to WSJTX-style ALL.txt file
37
37
  - 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
38
+ - Control rigs via Hamlib
40
39
 
41
40
  The Gui shows:
42
41
  - Simultaneous views of odd and even cycles
43
42
  - Messages overlaid on waterfall signals that produce them
44
43
  - 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
44
+ - 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
45
  - Marker on message if 2nd call is hearing you
47
46
  - Band activity in your level 4 square live updated next to band select buttons
48
47
  - 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 +62,7 @@ If you want to install this software without getting into the code, you can inst
63
62
  pip install PyFT8
64
63
  ```
65
64
 
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.
65
+ 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
66
 
68
67
  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
68
 
@@ -77,30 +76,19 @@ The use cases below are the ones held in the test cases at the bottom of the mai
77
76
  | Command line create a wav file | pyft8 -w "Mywav.wav" -m "CQ G1OJS IO90"| No config folder / .ini file required |
78
77
 
79
78
  ### 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.
79
+ 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
80
 
86
81
  ## 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.
82
+ 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
83
 
89
84
  ```
90
85
  [station]
91
86
  call = G1OJS
92
- grid = IO90******
87
+ grid = IO******
93
88
  [gui]
94
89
  loc = km_deg
95
90
  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]
91
+ [hamlib_rig]
104
92
  rigctld = C:/WSJT/wsjtx/bin/rigctld-wsjtx
105
93
  port = COM4
106
94
  baud_rate = 9600
@@ -16,14 +16,13 @@ If you're interested in how this works, maybe have a look at [MiniPyFT8](https:/
16
16
  - Finds sound cards by keywords so follows them if windows moves them ...
17
17
  - Logs QSOs to ADIF file and all spots to WSJTX-style ALL.txt file
18
18
  - 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
19
+ - Control rigs via Hamlib
21
20
 
22
21
  The Gui shows:
23
22
  - Simultaneous views of odd and even cycles
24
23
  - Messages overlaid on waterfall signals that produce them
25
24
  - 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
25
+ - 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
26
  - Marker on message if 2nd call is hearing you
28
27
  - Band activity in your level 4 square live updated next to band select buttons
29
28
  - 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 +43,7 @@ If you want to install this software without getting into the code, you can inst
44
43
  pip install PyFT8
45
44
  ```
46
45
 
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.
46
+ 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
47
 
49
48
  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
49
 
@@ -58,30 +57,19 @@ The use cases below are the ones held in the test cases at the bottom of the mai
58
57
  | Command line create a wav file | pyft8 -w "Mywav.wav" -m "CQ G1OJS IO90"| No config folder / .ini file required |
59
58
 
60
59
  ### 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.
60
+ 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
61
 
67
62
  ## 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.
63
+ 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
64
 
70
65
  ```
71
66
  [station]
72
67
  call = G1OJS
73
- grid = IO90******
68
+ grid = IO******
74
69
  [gui]
75
70
  loc = km_deg
76
71
  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]
72
+ [hamlib_rig]
85
73
  rigctld = C:/WSJT/wsjtx/bin/rigctld-wsjtx
86
74
  port = COM4
87
75
  baud_rate = 9600
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "PyFT8"
3
- version = "2.12.3"
3
+ version = "3.0.0"
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
File without changes
File without changes