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.
- {pyft8-2.12.3 → pyft8-3.0.0}/PKG-INFO +8 -20
- {pyft8-2.12.3 → pyft8-3.0.0}/PyFT8/databases.py +15 -16
- {pyft8-2.12.3 → pyft8-3.0.0}/PyFT8/pyft8.py +3 -9
- pyft8-3.0.0/PyFT8/rigctrl.py +43 -0
- {pyft8-2.12.3 → pyft8-3.0.0}/PyFT8.egg-info/PKG-INFO +8 -20
- {pyft8-2.12.3 → pyft8-3.0.0}/README.md +7 -19
- {pyft8-2.12.3 → pyft8-3.0.0}/pyproject.toml +1 -1
- pyft8-2.12.3/PyFT8/rigctrl.py +0 -102
- {pyft8-2.12.3 → pyft8-3.0.0}/LICENSE +0 -0
- {pyft8-2.12.3 → pyft8-3.0.0}/MANIFEST.in +0 -0
- {pyft8-2.12.3 → pyft8-3.0.0}/PyFT8/__init__.py +0 -0
- {pyft8-2.12.3 → pyft8-3.0.0}/PyFT8/gui.py +0 -0
- {pyft8-2.12.3 → pyft8-3.0.0}/PyFT8/pskreporter.py +0 -0
- {pyft8-2.12.3 → pyft8-3.0.0}/PyFT8/receiver.py +0 -0
- {pyft8-2.12.3 → pyft8-3.0.0}/PyFT8/time_utils.py +0 -0
- {pyft8-2.12.3 → pyft8-3.0.0}/PyFT8/transmitter.py +0 -0
- {pyft8-2.12.3 → pyft8-3.0.0}/PyFT8.egg-info/SOURCES.txt +0 -0
- {pyft8-2.12.3 → pyft8-3.0.0}/PyFT8.egg-info/dependency_links.txt +0 -0
- {pyft8-2.12.3 → pyft8-3.0.0}/PyFT8.egg-info/entry_points.txt +0 -0
- {pyft8-2.12.3 → pyft8-3.0.0}/PyFT8.egg-info/requires.txt +0 -0
- {pyft8-2.12.3 → pyft8-3.0.0}/PyFT8.egg-info/top_level.txt +0 -0
- {pyft8-2.12.3 → pyft8-3.0.0}/setup.cfg +0 -0
- {pyft8-2.12.3 → pyft8-3.0.0}/tests/dev/CQ AAAA.py +0 -0
- {pyft8-2.12.3 → pyft8-3.0.0}/tests/dev/osd.py +0 -0
- {pyft8-2.12.3 → pyft8-3.0.0}/tests/dev/test_generate_wav.py +0 -0
- {pyft8-2.12.3 → pyft8-3.0.0}/tests/dev/test_loopback_performance.py +0 -0
- {pyft8-2.12.3 → pyft8-3.0.0}/tests/dev/view_worked_before.py +0 -0
- {pyft8-2.12.3 → pyft8-3.0.0}/tests/plot_baseline.py +0 -0
- {pyft8-2.12.3 → pyft8-3.0.0}/tests/spare.py +0 -0
- {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:
|
|
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
|
-
-
|
|
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
|
|
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
|
|
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.
|
|
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 =
|
|
87
|
+
grid = IO******
|
|
93
88
|
[gui]
|
|
94
89
|
loc = km_deg
|
|
95
90
|
wb = Y
|
|
96
|
-
[
|
|
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.
|
|
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.
|
|
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
|
|
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.
|
|
183
|
-
self.
|
|
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.
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
self.
|
|
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.
|
|
237
|
-
|
|
238
|
-
if len(
|
|
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(
|
|
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.
|
|
252
|
-
rx_reports = self.
|
|
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
|
|
12
|
+
from PyFT8.rigctrl import Rig_hamlib
|
|
13
13
|
from PyFT8.databases import History, ADIF
|
|
14
14
|
|
|
15
|
-
VER = '
|
|
15
|
+
VER = '3.0.0'
|
|
16
16
|
|
|
17
17
|
MAX_TX_START_SECONDS = 2.5
|
|
18
|
-
HEARING_PANEL_LIFE_MINS =
|
|
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:
|
|
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
|
-
-
|
|
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
|
|
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
|
|
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.
|
|
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 =
|
|
87
|
+
grid = IO******
|
|
93
88
|
[gui]
|
|
94
89
|
loc = km_deg
|
|
95
90
|
wb = Y
|
|
96
|
-
[
|
|
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
|
-
-
|
|
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
|
|
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
|
|
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.
|
|
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 =
|
|
68
|
+
grid = IO******
|
|
74
69
|
[gui]
|
|
75
70
|
loc = km_deg
|
|
76
71
|
wb = Y
|
|
77
|
-
[
|
|
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
|
pyft8-2.12.3/PyFT8/rigctrl.py
DELETED
|
@@ -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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|