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.
- {pyft8-2.12.3 → pyft8-3.0.2}/PKG-INFO +11 -22
- {pyft8-2.12.3 → pyft8-3.0.2}/PyFT8/databases.py +18 -19
- {pyft8-2.12.3 → pyft8-3.0.2}/PyFT8/pskreporter.py +2 -1
- {pyft8-2.12.3 → pyft8-3.0.2}/PyFT8/pyft8.py +3 -9
- pyft8-3.0.2/PyFT8/rigctrl.py +43 -0
- {pyft8-2.12.3 → pyft8-3.0.2}/PyFT8/transmitter.py +68 -36
- {pyft8-2.12.3 → pyft8-3.0.2}/PyFT8.egg-info/PKG-INFO +11 -22
- {pyft8-2.12.3 → pyft8-3.0.2}/README.md +10 -21
- {pyft8-2.12.3 → pyft8-3.0.2}/pyproject.toml +1 -1
- pyft8-2.12.3/PyFT8/rigctrl.py +0 -102
- {pyft8-2.12.3 → pyft8-3.0.2}/LICENSE +0 -0
- {pyft8-2.12.3 → pyft8-3.0.2}/MANIFEST.in +0 -0
- {pyft8-2.12.3 → pyft8-3.0.2}/PyFT8/__init__.py +0 -0
- {pyft8-2.12.3 → pyft8-3.0.2}/PyFT8/gui.py +0 -0
- {pyft8-2.12.3 → pyft8-3.0.2}/PyFT8/receiver.py +0 -0
- {pyft8-2.12.3 → pyft8-3.0.2}/PyFT8/time_utils.py +0 -0
- {pyft8-2.12.3 → pyft8-3.0.2}/PyFT8.egg-info/SOURCES.txt +0 -0
- {pyft8-2.12.3 → pyft8-3.0.2}/PyFT8.egg-info/dependency_links.txt +0 -0
- {pyft8-2.12.3 → pyft8-3.0.2}/PyFT8.egg-info/entry_points.txt +0 -0
- {pyft8-2.12.3 → pyft8-3.0.2}/PyFT8.egg-info/requires.txt +0 -0
- {pyft8-2.12.3 → pyft8-3.0.2}/PyFT8.egg-info/top_level.txt +0 -0
- {pyft8-2.12.3 → pyft8-3.0.2}/setup.cfg +0 -0
- {pyft8-2.12.3 → pyft8-3.0.2}/tests/dev/CQ AAAA.py +0 -0
- {pyft8-2.12.3 → pyft8-3.0.2}/tests/dev/osd.py +0 -0
- {pyft8-2.12.3 → pyft8-3.0.2}/tests/dev/test_generate_wav.py +0 -0
- {pyft8-2.12.3 → pyft8-3.0.2}/tests/dev/test_loopback_performance.py +0 -0
- {pyft8-2.12.3 → pyft8-3.0.2}/tests/dev/view_worked_before.py +0 -0
- {pyft8-2.12.3 → pyft8-3.0.2}/tests/plot_baseline.py +0 -0
- {pyft8-2.12.3 → pyft8-3.0.2}/tests/spare.py +0 -0
- {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:
|
|
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
|
|
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
|
-
-
|
|
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
|
|
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
|
|
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.
|
|
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 =
|
|
88
|
+
grid = IO******
|
|
93
89
|
[gui]
|
|
94
90
|
loc = km_deg
|
|
95
91
|
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]
|
|
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|
|
|
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
|
-
|
|
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.
|
|
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,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
|
-
|
|
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
|
|
12
|
+
from PyFT8.rigctrl import Rig_hamlib
|
|
13
13
|
from PyFT8.databases import History, ADIF
|
|
14
14
|
|
|
15
|
-
VER = '
|
|
15
|
+
VER = '3.0.2'
|
|
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
|
+
|
|
@@ -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
|
|
60
|
-
|
|
61
|
-
|
|
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
|
-
|
|
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
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
|
100
|
-
if '/' in call
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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)
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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:
|
|
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
|
|
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
|
-
-
|
|
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
|
|
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
|
|
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.
|
|
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 =
|
|
88
|
+
grid = IO******
|
|
93
89
|
[gui]
|
|
94
90
|
loc = km_deg
|
|
95
91
|
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]
|
|
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|
|
|
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
|
|
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
|
-
-
|
|
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
|
|
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
|
|
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.
|
|
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 =
|
|
69
|
+
grid = IO******
|
|
74
70
|
[gui]
|
|
75
71
|
loc = km_deg
|
|
76
72
|
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]
|
|
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|
|
|
112
|
+
|4|NonStd Call |Y|N| <=11 char callsigns + hashed call|
|
|
124
113
|
|5|EU VHF | | | |
|
|
125
114
|
|
|
126
115
|
## Acknowledgements
|
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
|