PyFT8 2.1.3__tar.gz → 2.2.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.1.3 → pyft8-2.2.0}/PKG-INFO +9 -4
- {pyft8-2.1.3 → pyft8-2.2.0}/PyFT8/gui.py +52 -31
- {pyft8-2.1.3 → pyft8-2.2.0}/PyFT8/pyft8.py +41 -35
- {pyft8-2.1.3 → pyft8-2.2.0}/PyFT8/receiver.py +9 -1
- pyft8-2.2.0/PyFT8/rigctrl.py +72 -0
- {pyft8-2.1.3 → pyft8-2.2.0}/PyFT8.egg-info/PKG-INFO +9 -4
- pyft8-2.2.0/PyFT8.egg-info/SOURCES.txt +23 -0
- {pyft8-2.1.3 → pyft8-2.2.0}/README.md +8 -3
- {pyft8-2.1.3 → pyft8-2.2.0}/pyproject.toml +1 -1
- {pyft8-2.1.3 → pyft8-2.2.0}/tests/test_batch_and_live.py +11 -11
- pyft8-2.1.3/PyFT8/rigctrl.py +0 -67
- pyft8-2.1.3/PyFT8.egg-info/SOURCES.txt +0 -61
- pyft8-2.1.3/tests/data/ft8_lib_20m_busy/test_01.wav +0 -0
- pyft8-2.1.3/tests/data/ft8_lib_20m_busy/test_02.wav +0 -0
- pyft8-2.1.3/tests/data/ft8_lib_20m_busy/test_03.wav +0 -0
- pyft8-2.1.3/tests/data/ft8_lib_20m_busy/test_04.wav +0 -0
- pyft8-2.1.3/tests/data/ft8_lib_20m_busy/test_05.wav +0 -0
- pyft8-2.1.3/tests/data/ft8_lib_20m_busy/test_06.wav +0 -0
- pyft8-2.1.3/tests/data/ft8_lib_20m_busy/test_07.wav +0 -0
- pyft8-2.1.3/tests/data/ft8_lib_20m_busy/test_08.wav +0 -0
- pyft8-2.1.3/tests/data/ft8_lib_20m_busy/test_09.wav +0 -0
- pyft8-2.1.3/tests/data/ft8_lib_20m_busy/test_10.wav +0 -0
- pyft8-2.1.3/tests/data/ft8_lib_20m_busy/test_11.wav +0 -0
- pyft8-2.1.3/tests/data/ft8_lib_20m_busy/test_12.wav +0 -0
- pyft8-2.1.3/tests/data/ft8_lib_20m_busy/test_13.wav +0 -0
- pyft8-2.1.3/tests/data/ft8_lib_20m_busy/test_14.wav +0 -0
- pyft8-2.1.3/tests/data/ft8_lib_20m_busy/test_15.wav +0 -0
- pyft8-2.1.3/tests/data/ft8_lib_20m_busy/test_16.wav +0 -0
- pyft8-2.1.3/tests/data/ft8_lib_20m_busy/test_17.wav +0 -0
- pyft8-2.1.3/tests/data/ft8_lib_20m_busy/test_18.wav +0 -0
- pyft8-2.1.3/tests/data/ft8_lib_20m_busy/test_19.wav +0 -0
- pyft8-2.1.3/tests/data/ft8_lib_20m_busy/test_20.wav +0 -0
- pyft8-2.1.3/tests/data/ft8_lib_20m_busy/test_21.wav +0 -0
- pyft8-2.1.3/tests/data/ft8_lib_20m_busy/test_22.wav +0 -0
- pyft8-2.1.3/tests/data/ft8_lib_20m_busy/test_23.wav +0 -0
- pyft8-2.1.3/tests/data/ft8_lib_20m_busy/test_24.wav +0 -0
- pyft8-2.1.3/tests/data/ft8_lib_20m_busy/test_25.wav +0 -0
- pyft8-2.1.3/tests/data/ft8_lib_20m_busy/test_26.wav +0 -0
- pyft8-2.1.3/tests/data/ft8_lib_20m_busy/test_27.wav +0 -0
- pyft8-2.1.3/tests/data/ft8_lib_20m_busy/test_28.wav +0 -0
- pyft8-2.1.3/tests/data/ft8_lib_20m_busy/test_29.wav +0 -0
- pyft8-2.1.3/tests/data/ft8_lib_20m_busy/test_30.wav +0 -0
- pyft8-2.1.3/tests/data/ft8_lib_20m_busy/test_31.wav +0 -0
- pyft8-2.1.3/tests/data/ft8_lib_20m_busy/test_32.wav +0 -0
- pyft8-2.1.3/tests/data/ft8_lib_20m_busy/test_33.wav +0 -0
- pyft8-2.1.3/tests/data/ft8_lib_20m_busy/test_34.wav +0 -0
- pyft8-2.1.3/tests/data/ft8_lib_20m_busy/test_35.wav +0 -0
- pyft8-2.1.3/tests/data/ft8_lib_20m_busy/test_36.wav +0 -0
- pyft8-2.1.3/tests/data/ft8_lib_20m_busy/test_37.wav +0 -0
- pyft8-2.1.3/tests/data/ft8_lib_20m_busy/test_38.wav +0 -0
- {pyft8-2.1.3 → pyft8-2.2.0}/LICENSE +0 -0
- {pyft8-2.1.3 → pyft8-2.2.0}/MANIFEST.in +0 -0
- {pyft8-2.1.3 → pyft8-2.2.0}/PyFT8/__init__.py +0 -0
- {pyft8-2.1.3 → pyft8-2.2.0}/PyFT8/time_utils.py +0 -0
- {pyft8-2.1.3 → pyft8-2.2.0}/PyFT8/transmitter.py +0 -0
- {pyft8-2.1.3 → pyft8-2.2.0}/PyFT8.egg-info/dependency_links.txt +0 -0
- {pyft8-2.1.3 → pyft8-2.2.0}/PyFT8.egg-info/entry_points.txt +0 -0
- {pyft8-2.1.3 → pyft8-2.2.0}/PyFT8.egg-info/requires.txt +0 -0
- {pyft8-2.1.3 → pyft8-2.2.0}/PyFT8.egg-info/top_level.txt +0 -0
- {pyft8-2.1.3 → pyft8-2.2.0}/setup.cfg +0 -0
- {pyft8-2.1.3 → pyft8-2.2.0}/tests/dev/osd.py +0 -0
- {pyft8-2.1.3 → pyft8-2.2.0}/tests/dev/test_generate_wav.py +0 -0
- {pyft8-2.1.3 → pyft8-2.2.0}/tests/dev/test_loopback_performance.py +0 -0
- {pyft8-2.1.3 → pyft8-2.2.0}/tests/plot_baseline.py +0 -0
- {pyft8-2.1.3 → pyft8-2.2.0}/tests/spare.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: PyFT8
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.2.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
|
|
@@ -34,7 +34,8 @@ PyFT8 is somewhat experimental, with a focus on demonstrating FT8 written in Pyt
|
|
|
34
34
|
- Modern programming language throughout
|
|
35
35
|
- Finds sound cards by keywords so follows them if windows moves them ...
|
|
36
36
|
|
|
37
|
-
<img width="
|
|
37
|
+
<img width="1003" height="1020" alt="image" src="https://github.com/user-attachments/assets/bf6e3f78-531a-4c9b-ab2b-b51cc04ad980" />
|
|
38
|
+
|
|
38
39
|
|
|
39
40
|
|
|
40
41
|
## Motivation
|
|
@@ -61,7 +62,9 @@ Once installed, you can use the following commands to run it. Otherwise, please
|
|
|
61
62
|
| Launch configured GUI|pyft8 -i "Keyword1, Keyword2" -o "Keyword1, Keyword2" -c {config folder}| Config folder stores PyFT8.ini (your callsign, grid, buttons) and PyFT8.adi log file. Run this once to create default PyFT8.ini file.|
|
|
62
63
|
|
|
63
64
|
### Rig control
|
|
64
|
-
|
|
65
|
+
I'm resisting interfacing this to middleware such as Hamlib and Omnirig, because I want to keep the whole thing self-contained and simple, and losing connections to/from middleware was one motivation for writing this in the first place.
|
|
66
|
+
|
|
67
|
+
However, 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.
|
|
65
68
|
|
|
66
69
|
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.
|
|
67
70
|
|
|
@@ -69,9 +72,11 @@ Alternatively, you can run PyFT8 without rig control; if there is no rig found,
|
|
|
69
72
|
|
|
70
73
|
The image below shows the number of decodes from PyFT8, WSJT-x V2.7.0 running in NORM mode, and FT8_lib, using the same 10 minutes of busy 20m audio that is used to test ft8_lib.
|
|
71
74
|
|
|
72
|
-
<img width="
|
|
75
|
+
<img width="640" height="480" alt="performance snapshot" src="https://github.com/user-attachments/assets/fc84702a-4d76-475b-b7fe-489f4a09deed" />
|
|
76
|
+
|
|
73
77
|
|
|
74
78
|
## Limitations
|
|
79
|
+
|
|
75
80
|
In pursuit of tight code, I've concentrated on core standard messages, leaving out some of the less-used features. The receive part of the
|
|
76
81
|
code doesn't (yet) have the full capability of the advanced decoders used in WSJT-x, and so gets fewer decodes than WSJT-x gets, depending on band conditions (on a quiet band with only good signals PyFT8 will get close to 100%).
|
|
77
82
|
|
|
@@ -8,6 +8,29 @@ from matplotlib.widgets import Slider, Button
|
|
|
8
8
|
rcParams['toolbar'] = 'None'
|
|
9
9
|
# ================== WATERFALL ======================================================
|
|
10
10
|
|
|
11
|
+
class Scrollbox:
|
|
12
|
+
def __init__(self, fig, ax, nlines = 5):
|
|
13
|
+
self.fig, self.ax = fig, ax
|
|
14
|
+
bbox = ax.get_window_extent().transformed(fig.dpi_scale_trans.inverted())
|
|
15
|
+
self.fontsize = 0.5 * bbox.height * fig.dpi / nlines
|
|
16
|
+
self.nlines = nlines
|
|
17
|
+
self.line_height = 0.9 / nlines
|
|
18
|
+
self.lines = []
|
|
19
|
+
self.lineartists = []
|
|
20
|
+
for i in range(self.nlines):
|
|
21
|
+
self.lineartists.append(self.ax.text(0.03,1 - self.line_height * (i+1),
|
|
22
|
+
'', color = 'white', fontsize = self.fontsize))
|
|
23
|
+
self.ax.set_xticks([])
|
|
24
|
+
self.ax.set_yticks([])
|
|
25
|
+
self.ax.set_facecolor('black')
|
|
26
|
+
|
|
27
|
+
def print(self, text, color = 'white'):
|
|
28
|
+
self.lines = self.lines[-(self.nlines-1):]
|
|
29
|
+
self.lines.append({'text':text, 'color':color})
|
|
30
|
+
for i, line in enumerate(self.lines):
|
|
31
|
+
self.lineartists[i].set_text(line['text'])
|
|
32
|
+
self.lineartists[i].set_color(line['color'])
|
|
33
|
+
|
|
11
34
|
class Msg_box:
|
|
12
35
|
def __init__(self, fig, ax, tbin, fbin, w, h, onclick):
|
|
13
36
|
from matplotlib.patches import Rectangle
|
|
@@ -47,50 +70,48 @@ class Msg_box:
|
|
|
47
70
|
|
|
48
71
|
class Gui:
|
|
49
72
|
def __init__(self, dBgrid, hps, bpt, config, on_msg_click, on_control_click):
|
|
50
|
-
|
|
73
|
+
if config is not None:
|
|
74
|
+
self.mStation = {'c':config['station']['call'], 'g':config['station']['grid']}
|
|
51
75
|
self.on_msg_click = on_msg_click
|
|
52
76
|
self.on_control_click = on_control_click
|
|
53
77
|
self.dBgrid = dBgrid
|
|
54
78
|
self.hps, self.bpt = hps, bpt
|
|
55
79
|
self.msg_boxes = {}
|
|
56
80
|
self.decode_queue = queue.Queue()
|
|
57
|
-
self.
|
|
81
|
+
self.pmarg = 0.04
|
|
58
82
|
self.make_layout(config)
|
|
83
|
+
self.ani = FuncAnimation(self.fig, self._animate, interval = 40, frames=(100000), blit=True)
|
|
59
84
|
|
|
60
|
-
def
|
|
61
|
-
if self.simple_message_art:
|
|
62
|
-
self.simple_message_art.remove()
|
|
63
|
-
self.simple_message_art = self.fig.text(0.2,0.985, text, color = color)
|
|
64
|
-
self.fig.canvas.draw()
|
|
65
|
-
self.fig.canvas.flush_events()
|
|
66
|
-
|
|
67
|
-
def make_layout(self, config):
|
|
68
|
-
self.fig, self.ax_wf = plt.subplots(figsize=(10,10), frameon = False)
|
|
69
|
-
self.fig.canvas.manager.set_window_title('PyFT8 by G1OJS')
|
|
85
|
+
def make_layout(self, config, wf_left = 0.15, wf_top = 0.87):
|
|
70
86
|
self.plt = plt
|
|
71
|
-
plt.
|
|
72
|
-
self.
|
|
73
|
-
|
|
74
|
-
self.ax_wf.
|
|
75
|
-
|
|
87
|
+
self.fig = plt.figure(figsize = (10,10), facecolor=(.18, .71, .71, 0.4))
|
|
88
|
+
self.fig.canvas.manager.set_window_title('PyFT8 by G1OJS')
|
|
89
|
+
self.ax_wf = self.fig.add_axes([self.pmarg + wf_left, self.pmarg, 1-2*self.pmarg-wf_left, wf_top-self.pmarg])
|
|
90
|
+
self.image = self.ax_wf.imshow(self.dBgrid.T,vmax=120,vmin=90,origin='lower',interpolation='none', aspect = 'auto')
|
|
91
|
+
self.ax_wf.set_xticks([])
|
|
92
|
+
self.ax_wf.set_yticks([])
|
|
93
|
+
self.ax_console = self.fig.add_axes([self.pmarg + wf_left, wf_top, 1-2*self.pmarg - wf_left, 1-self.pmarg-wf_top])
|
|
94
|
+
self.console = Scrollbox(self.fig, self.ax_console)
|
|
95
|
+
|
|
96
|
+
if config is not None:
|
|
97
|
+
styles = {'ctrl':{'fc':'grey','c':'black'}, 'band':{'fc':'green','c':'white'}}
|
|
98
|
+
button_defs = [{'label':'CQ','style':'ctrl','data':None}, {'label':'Repeat last','style':'ctrl','data':None},
|
|
99
|
+
{'label':'Tx off','style':'ctrl','data':None}]
|
|
100
|
+
#{'label':'Averaging','style':'ctrl','data':None}]
|
|
101
|
+
for band, freq in config['bands'].items():
|
|
102
|
+
button_defs.append({'label':band,'style':'band','data':freq})
|
|
103
|
+
self._make_buttons(button_defs, styles, wf_top, 0.02, 0.1, 0.002)
|
|
104
|
+
|
|
105
|
+
def _make_buttons(self, buttons, styles, btns_top, btn_h, btn_w, sep_h):
|
|
76
106
|
self.buttons = []
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
{'label':'Tx off','style':'ctrl','data':None}]
|
|
80
|
-
#{'label':'Averaging','style':'ctrl','data':None}]
|
|
81
|
-
for band, freq in config['bands'].items():
|
|
82
|
-
control_buttons.append({'label':band,'style':'band','data':freq})
|
|
83
|
-
|
|
84
|
-
btn_axs = []
|
|
85
|
-
for i, btn in enumerate(control_buttons):
|
|
86
|
-
btn_axs.append(plt.axes([0.05, 0.9 - 0.022 * i, 0.1, 0.02]))
|
|
107
|
+
for i, btn in enumerate(buttons):
|
|
108
|
+
btn_axs = plt.axes([self.pmarg, btns_top - (i+1) * btn_h, btn_w, btn_h-sep_h])
|
|
87
109
|
style = styles[btn['style']]
|
|
88
|
-
btn_widg = Button(btn_axs
|
|
110
|
+
btn_widg = Button(btn_axs, btn['label'], color=style['fc'], hovercolor='skyblue')
|
|
89
111
|
btn_widg.data = btn['data']
|
|
90
112
|
btn_widg.on_clicked(lambda event, btn_widg=btn_widg: self.on_control_click(btn_widg))
|
|
91
113
|
self.buttons.append(btn_widg)
|
|
92
|
-
|
|
93
|
-
|
|
114
|
+
|
|
94
115
|
def add_message_box(self, message):
|
|
95
116
|
self.decode_queue.put(message)
|
|
96
117
|
|
|
@@ -111,6 +132,6 @@ class Gui:
|
|
|
111
132
|
self._display_message_box(self.decode_queue.get())
|
|
112
133
|
if (frame % 10 == 0):
|
|
113
134
|
self._tidy_msg_boxes()
|
|
114
|
-
return [self.image, *self.ax_wf.patches, *self.ax_wf.texts]
|
|
135
|
+
return [self.image, *self.ax_wf.patches, *self.ax_wf.texts, *self.console.lineartists]
|
|
115
136
|
|
|
116
137
|
|
|
@@ -8,21 +8,12 @@ from PyFT8.receiver import Receiver, AudioIn
|
|
|
8
8
|
from PyFT8.gui import Gui
|
|
9
9
|
from PyFT8.transmitter import AudioOut
|
|
10
10
|
from PyFT8.time_utils import global_time_utils
|
|
11
|
+
from PyFT8.rigctrl import Rig
|
|
11
12
|
|
|
12
13
|
MAX_TX_START_SECONDS = 2.5
|
|
13
14
|
T_CYC = 15
|
|
14
|
-
rig = None
|
|
15
|
-
gui = None
|
|
15
|
+
rig, gui, qso, worked_before = None, None, None, None
|
|
16
16
|
|
|
17
|
-
def load_rigctrl():
|
|
18
|
-
try:
|
|
19
|
-
from PyFT8.rigctrl import Rig
|
|
20
|
-
print("Loaded Rig control")
|
|
21
|
-
return Rig()
|
|
22
|
-
except ImportError:
|
|
23
|
-
print("No Rig control found")
|
|
24
|
-
return None
|
|
25
|
-
|
|
26
17
|
def get_config(config_folder):
|
|
27
18
|
import configparser
|
|
28
19
|
global config
|
|
@@ -31,10 +22,13 @@ def get_config(config_folder):
|
|
|
31
22
|
if not os.path.exists(ini_file):
|
|
32
23
|
config['station'] = {'call':'station_callsign', 'grid':'station_grid'}
|
|
33
24
|
config['bands'] = {'20m':14.074}
|
|
25
|
+
config['rig'] = {'port': 'COM4', 'baud_rate':9600,
|
|
26
|
+
'set_freq_command':'FEFE88E0.05.0000000000.FD', 'set_freq_value':'5|5|vfBcdLU|1|0',
|
|
27
|
+
'ptt_on_command':'FEFE88E0.1C00.01.FD', 'ptt_off_command':'FEFE88E0.1C00.00.FD'}
|
|
34
28
|
with open(ini_file, 'w') as f:
|
|
35
29
|
config.write(f)
|
|
36
|
-
|
|
37
|
-
|
|
30
|
+
console_print(f"Wrote default config to {ini_file}")
|
|
31
|
+
console_print(f"Reading config from {ini_file}")
|
|
38
32
|
config.read(ini_file)
|
|
39
33
|
|
|
40
34
|
def parse_from_adif_rec(rec, field):
|
|
@@ -97,13 +91,15 @@ class Logging:
|
|
|
97
91
|
f.write(f"<{k}:{len(v)}>{v} ")
|
|
98
92
|
f.write(f"<eor>\n")
|
|
99
93
|
self.update_worked_before(oStation['c'], time.time())
|
|
100
|
-
|
|
94
|
+
console_print(f"Logged QSO with {oStation['c']}")
|
|
101
95
|
|
|
102
96
|
|
|
103
97
|
class Message:
|
|
104
98
|
def __init__(self, candidate):
|
|
105
99
|
c = candidate
|
|
106
|
-
mycall =
|
|
100
|
+
mycall = ''
|
|
101
|
+
if qso is not None:
|
|
102
|
+
mycall = qso.mStation['c']
|
|
107
103
|
self.h0_idx, self.f0_idx, self.msg_tuple, self.msg, self.snr, self.dt, self.fHz = c.h0_idx, c.f0_idx, c.msg_tuple, c.msg, c.snr, c.dt, c.fHz
|
|
108
104
|
self.cyclestart = c.cyclestart
|
|
109
105
|
self.expire = time.time() + 29.8
|
|
@@ -111,7 +107,7 @@ class Message:
|
|
|
111
107
|
self.is_to_me = c.msg_tuple[0] == mycall
|
|
112
108
|
self.is_cq = c.msg_tuple[0].startswith('CQ')
|
|
113
109
|
gui_wb_text = ''
|
|
114
|
-
if self.is_cq:
|
|
110
|
+
if self.is_cq and worked_before is not None:
|
|
115
111
|
if c.msg_tuple[1] in worked_before:
|
|
116
112
|
gui_wb_text = f"wb: {global_time_utils.format_duration(time.time() - worked_before[c.msg_tuple[1]])}"
|
|
117
113
|
self.gui_text = f"{c.msg} {gui_wb_text}"
|
|
@@ -123,7 +119,8 @@ class Message:
|
|
|
123
119
|
class FT8_QSO:
|
|
124
120
|
def __init__(self, logging):
|
|
125
121
|
self.logging = logging
|
|
126
|
-
|
|
122
|
+
if config is not None:
|
|
123
|
+
self.mStation = {'c':config['station']['call'], 'g':config['station']['grid']}
|
|
127
124
|
self.band_info = {'b':None, 'f':0}
|
|
128
125
|
self.tx_freq = 750
|
|
129
126
|
threading.Thread(target = self._transmitter, daemon = True).start()
|
|
@@ -139,9 +136,9 @@ class FT8_QSO:
|
|
|
139
136
|
|
|
140
137
|
def set_tx_message(self, message):
|
|
141
138
|
if gui and self.band_info['b'] is None:
|
|
142
|
-
|
|
139
|
+
console_print("[PyFT8] Please select a band before transmitting", color = 'red')
|
|
143
140
|
return
|
|
144
|
-
|
|
141
|
+
console_print(f"[QSO] Set transmit message to '{message}' with tx cycle = {self.tx_cycle}")
|
|
145
142
|
self.message_to_transmit = message
|
|
146
143
|
|
|
147
144
|
def _transmitter(self):
|
|
@@ -150,7 +147,7 @@ class FT8_QSO:
|
|
|
150
147
|
if self.message_to_transmit is None:
|
|
151
148
|
continue
|
|
152
149
|
if output_device_idx is None:
|
|
153
|
-
|
|
150
|
+
console_print("No output device")
|
|
154
151
|
return
|
|
155
152
|
ct = global_time_utils.cycle_time()
|
|
156
153
|
if ct > MAX_TX_START_SECONDS:
|
|
@@ -159,17 +156,18 @@ class FT8_QSO:
|
|
|
159
156
|
if self.tx_cycle is None:
|
|
160
157
|
self.tx_cycle = global_time_utils.curr_cycle_from_time()
|
|
161
158
|
self.tx_freq = clear_frequencies[self.tx_cycle]
|
|
162
|
-
|
|
159
|
+
console_print(f"Transmitting {self.message_to_transmit} on cycle {self.tx_cycle}")
|
|
163
160
|
symbols = audio_out.create_ft8_symbols(self.message_to_transmit)
|
|
164
161
|
audio_data = audio_out.create_ft8_wave(symbols, f_base = self.tx_freq)
|
|
165
|
-
rig.
|
|
162
|
+
rig.ptt_on()
|
|
166
163
|
audio_out.play_data_to_soundcard(audio_data, output_device_idx)
|
|
167
|
-
rig.
|
|
164
|
+
rig.ptt_off()
|
|
168
165
|
self.last_tx = self.message_to_transmit
|
|
169
166
|
self.message_to_transmit = None
|
|
170
167
|
|
|
171
168
|
def log(self):
|
|
172
|
-
self.logging
|
|
169
|
+
if self.logging is not None:
|
|
170
|
+
self.logging.log(self.times, self.band_info, self.mStation, self.oStation, self.rpts)
|
|
173
171
|
|
|
174
172
|
def isReport(grid_rpt): return "+" in grid_rpt or "-" in grid_rpt
|
|
175
173
|
def isRReport(grid_rpt): return isReport(grid_rpt) and 'R' in grid_rpt
|
|
@@ -182,12 +180,14 @@ def progress_qso(clicked_message):
|
|
|
182
180
|
global qso
|
|
183
181
|
|
|
184
182
|
if time.time() - clicked_message.cyclestart['time'] > (15 + MAX_TX_START_SECONDS):
|
|
185
|
-
|
|
183
|
+
console_print("Try next cycle")
|
|
186
184
|
return
|
|
187
185
|
|
|
188
186
|
call_a, call_b, grid_rpt = clicked_message.msg_tuple
|
|
189
187
|
my_station = qso.mStation
|
|
190
188
|
reply = ""
|
|
189
|
+
msg = ' '.join(clicked_message.msg_tuple)
|
|
190
|
+
console_print(f"[QSO] Clicked on message '{msg}'")
|
|
191
191
|
|
|
192
192
|
if call_a == "CQ":
|
|
193
193
|
qso.clear()
|
|
@@ -212,7 +212,7 @@ def progress_qso(clicked_message):
|
|
|
212
212
|
if isRR73(grid_rpt):
|
|
213
213
|
reply = f"{qso.oStation['c']} {my_station['c']} 73"
|
|
214
214
|
qso.set_tx_message(reply)
|
|
215
|
-
|
|
215
|
+
|
|
216
216
|
if is73(grid_rpt) or " 73" in reply or isRR73(grid_rpt):
|
|
217
217
|
qso.times['time_off'] = time.gmtime()
|
|
218
218
|
qso.log()
|
|
@@ -221,7 +221,7 @@ def make_wav(msg, wave_output_file): # move to transmitter.py?
|
|
|
221
221
|
symbols = audio_out.create_ft8_symbols(msg)
|
|
222
222
|
audio_data = audio_out.create_ft8_wave(symbols)
|
|
223
223
|
audio_out.write_to_wave_file(audio_data, wave_output_file)
|
|
224
|
-
|
|
224
|
+
console_print(f"Created wave file {wave_output_file}")
|
|
225
225
|
|
|
226
226
|
def wait_for_keyboard():
|
|
227
227
|
import time
|
|
@@ -245,7 +245,7 @@ def on_busy_profile(busy_profile, cycle):
|
|
|
245
245
|
f0_idx, fn_idx = int(500/audio_in.df), int(fmax/audio_in.df)
|
|
246
246
|
idx = np.argmin(busy_profile[f0_idx:fn_idx])
|
|
247
247
|
clear_frequencies[cycle] = (f0_idx + idx) * audio_in.df
|
|
248
|
-
|
|
248
|
+
console_print(f"[on_busy] Set Tx freq to {clear_frequencies[cycle]:6.1f} for cycle {cycle}")
|
|
249
249
|
|
|
250
250
|
def on_control_click(btn_widg):
|
|
251
251
|
btn_text, btn_data = btn_widg.label.get_text(), btn_widg.data
|
|
@@ -255,16 +255,23 @@ def on_control_click(btn_widg):
|
|
|
255
255
|
if btn_text == "Repeat last":
|
|
256
256
|
qso.set_tx_message(qso.last_tx)
|
|
257
257
|
if btn_text == "Tx off":
|
|
258
|
-
|
|
258
|
+
console_print("[PyFT8] Set PTT Off")
|
|
259
|
+
rig.ptt_off()
|
|
259
260
|
if('m' in btn_text):
|
|
260
261
|
qso.band_info = {'b':btn_text, 'f':btn_data}
|
|
261
|
-
rig.
|
|
262
|
-
|
|
262
|
+
rig.set_freq_Hz(int(1000000*float(qso.band_info['f'])))
|
|
263
|
+
console_print(f"[PyFT8] Set band: {qso.band_info['b']} {qso.band_info['f']}")
|
|
263
264
|
|
|
264
265
|
def on_msg_click(message):
|
|
265
266
|
progress_qso(message)
|
|
266
267
|
|
|
267
|
-
#=============== CLI ========================================================================
|
|
268
|
+
#=============== CLI ========================================================================
|
|
269
|
+
def console_print(text, color = 'white'):
|
|
270
|
+
if gui is not None:
|
|
271
|
+
gui.console.print(text, color)
|
|
272
|
+
else:
|
|
273
|
+
print(text)
|
|
274
|
+
|
|
268
275
|
def cli():
|
|
269
276
|
global audio_in, audio_out, output_device_idx, rig, gui, qso, config, clear_frequencies
|
|
270
277
|
import time
|
|
@@ -282,9 +289,8 @@ def cli():
|
|
|
282
289
|
config_folder = f"{args.config_folder}".strip()
|
|
283
290
|
get_config(config_folder)
|
|
284
291
|
logging = Logging(config_folder)
|
|
285
|
-
#print(worked_before)
|
|
286
292
|
qso = FT8_QSO(logging)
|
|
287
|
-
rig =
|
|
293
|
+
rig = Rig(config)
|
|
288
294
|
|
|
289
295
|
if args.transmit_message or args.outputcard_keywords:
|
|
290
296
|
audio_out = AudioOut()
|
|
@@ -303,7 +309,7 @@ def cli():
|
|
|
303
309
|
audio_in = AudioIn(3100)
|
|
304
310
|
input_device_idx = audio_in.find_device(args.inputcard_keywords.replace(' ','').split(','))
|
|
305
311
|
if not input_device_idx:
|
|
306
|
-
|
|
312
|
+
console_print("No input device")
|
|
307
313
|
else:
|
|
308
314
|
gui = None if args.no_gui else Gui(audio_in.dBgrid_main, 4, 2, config, on_msg_click, on_control_click)
|
|
309
315
|
rx = Receiver(audio_in, [200, 3100], on_decode, on_busy_profile)
|
|
@@ -275,8 +275,16 @@ class Candidate:
|
|
|
275
275
|
bits77_int = check_crc(bits91_int)
|
|
276
276
|
if(bits77_int):
|
|
277
277
|
self.msg_tuple = unpack(bits77_int)
|
|
278
|
-
self.msg =
|
|
278
|
+
self.msg = self.validate(self.msg_tuple)
|
|
279
279
|
self.decode_completed = time.time()
|
|
280
|
+
|
|
281
|
+
def validate(self, msg_tuple):
|
|
282
|
+
mt = msg_tuple
|
|
283
|
+
e = False
|
|
284
|
+
e = e or (' ' in mt[0].strip() and not mt[0].startswith('CQ'))
|
|
285
|
+
#e = e or (' ' in mt[1].strip())
|
|
286
|
+
if not e:
|
|
287
|
+
return ' '.join(self.msg_tuple)
|
|
280
288
|
|
|
281
289
|
#============== RECEIVER ===========================================================
|
|
282
290
|
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import serial, time
|
|
2
|
+
|
|
3
|
+
class Rig:
|
|
4
|
+
|
|
5
|
+
def __init__(self, config, verbose = False):
|
|
6
|
+
self.serial_port = False
|
|
7
|
+
self.port = config['rig']['port']
|
|
8
|
+
self.baud_rate = config['rig']['baud_rate']
|
|
9
|
+
self.ptt_on_cmd = self.parse_configstr(config['rig']['ptt_on_command']) if config['rig']['ptt_on_command']else None
|
|
10
|
+
self.ptt_off_cmd = self.parse_configstr(config['rig']['ptt_off_command']) if config['rig']['ptt_off_command'] else None
|
|
11
|
+
self.set_freq_cmd = self.parse_configstr(config['rig']['set_freq_command']) if config['rig']['set_freq_command'] else None
|
|
12
|
+
self.set_freq_value = config['rig']['set_freq_value'] if config['rig']['set_freq_value'] else None
|
|
13
|
+
self.verbose = verbose
|
|
14
|
+
|
|
15
|
+
def parse_configstr(self, configstr):
|
|
16
|
+
if "." in configstr:
|
|
17
|
+
hexstr = configstr.replace(".", "")
|
|
18
|
+
return bytearray.fromhex(hexstr)
|
|
19
|
+
else:
|
|
20
|
+
return bytearray(configstr.encode())
|
|
21
|
+
|
|
22
|
+
def vprint(self, text):
|
|
23
|
+
if self.verbose:
|
|
24
|
+
print(text)
|
|
25
|
+
|
|
26
|
+
def _sendCAT(self, msg):
|
|
27
|
+
try:
|
|
28
|
+
self.serial_port = serial.Serial(port = self.port, baudrate = self.baud_rate, timeout = 0.1)
|
|
29
|
+
except Exception as e:
|
|
30
|
+
print(f"[CAT] couldn't open {self.port}: {e}")
|
|
31
|
+
if (self.serial_port):
|
|
32
|
+
self.serial_port.reset_input_buffer()
|
|
33
|
+
self.vprint(f"[CAT] send {msg.hex(' ')}")
|
|
34
|
+
try:
|
|
35
|
+
self.serial_port.write(msg)
|
|
36
|
+
time.sleep(0.05)
|
|
37
|
+
self.serial_port.close()
|
|
38
|
+
except Exception as e:
|
|
39
|
+
print(f"[CAT] couldn't send CAT command {msg} on {self.port}: {e}")
|
|
40
|
+
|
|
41
|
+
def set_freq_Hz(self, freqHz):
|
|
42
|
+
if self.set_freq_cmd and self.set_freq_value:
|
|
43
|
+
self.vprint(f"[CAT] SET frequency to {freqHz} Hz")
|
|
44
|
+
start, length, fmt, mult, offset = self.set_freq_value.split("|")
|
|
45
|
+
start, length, mult, offset = int(start), int(length), int(mult), int(offset)
|
|
46
|
+
fVal = freqHz * mult + offset
|
|
47
|
+
nDigits = length if fmt == "text" else 2*length
|
|
48
|
+
s = f"{fVal:0{nDigits}d}"
|
|
49
|
+
if fmt=='text':
|
|
50
|
+
fBytes = s.encode()
|
|
51
|
+
else:
|
|
52
|
+
pairs = [(int(s[i]) << 4) | int(s[i+1]) for i in range(0, len(s), 2)]
|
|
53
|
+
if fmt == "vfBcdLU":
|
|
54
|
+
fBytes = bytes(pairs[::-1])
|
|
55
|
+
else:
|
|
56
|
+
fBytes = bytes(pairs)
|
|
57
|
+
cmd = bytearray(self.set_freq_cmd)
|
|
58
|
+
cmd[start:start+length] = fBytes
|
|
59
|
+
if fmt.startswith("vfBcd"): # CI-V
|
|
60
|
+
cmd = b'\x00' + cmd
|
|
61
|
+
self._sendCAT(cmd)
|
|
62
|
+
|
|
63
|
+
def ptt_on(self):
|
|
64
|
+
if self.ptt_on_cmd:
|
|
65
|
+
self.vprint(f"[CAT] PTT On")
|
|
66
|
+
self._sendCAT(self.ptt_on_cmd)
|
|
67
|
+
|
|
68
|
+
def ptt_off(self):
|
|
69
|
+
if self.ptt_off_cmd:
|
|
70
|
+
self.vprint(f"[CAT] PTT Off")
|
|
71
|
+
self._sendCAT(self.ptt_off_cmd)
|
|
72
|
+
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: PyFT8
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.2.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
|
|
@@ -34,7 +34,8 @@ PyFT8 is somewhat experimental, with a focus on demonstrating FT8 written in Pyt
|
|
|
34
34
|
- Modern programming language throughout
|
|
35
35
|
- Finds sound cards by keywords so follows them if windows moves them ...
|
|
36
36
|
|
|
37
|
-
<img width="
|
|
37
|
+
<img width="1003" height="1020" alt="image" src="https://github.com/user-attachments/assets/bf6e3f78-531a-4c9b-ab2b-b51cc04ad980" />
|
|
38
|
+
|
|
38
39
|
|
|
39
40
|
|
|
40
41
|
## Motivation
|
|
@@ -61,7 +62,9 @@ Once installed, you can use the following commands to run it. Otherwise, please
|
|
|
61
62
|
| Launch configured GUI|pyft8 -i "Keyword1, Keyword2" -o "Keyword1, Keyword2" -c {config folder}| Config folder stores PyFT8.ini (your callsign, grid, buttons) and PyFT8.adi log file. Run this once to create default PyFT8.ini file.|
|
|
62
63
|
|
|
63
64
|
### Rig control
|
|
64
|
-
|
|
65
|
+
I'm resisting interfacing this to middleware such as Hamlib and Omnirig, because I want to keep the whole thing self-contained and simple, and losing connections to/from middleware was one motivation for writing this in the first place.
|
|
66
|
+
|
|
67
|
+
However, 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.
|
|
65
68
|
|
|
66
69
|
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.
|
|
67
70
|
|
|
@@ -69,9 +72,11 @@ Alternatively, you can run PyFT8 without rig control; if there is no rig found,
|
|
|
69
72
|
|
|
70
73
|
The image below shows the number of decodes from PyFT8, WSJT-x V2.7.0 running in NORM mode, and FT8_lib, using the same 10 minutes of busy 20m audio that is used to test ft8_lib.
|
|
71
74
|
|
|
72
|
-
<img width="
|
|
75
|
+
<img width="640" height="480" alt="performance snapshot" src="https://github.com/user-attachments/assets/fc84702a-4d76-475b-b7fe-489f4a09deed" />
|
|
76
|
+
|
|
73
77
|
|
|
74
78
|
## Limitations
|
|
79
|
+
|
|
75
80
|
In pursuit of tight code, I've concentrated on core standard messages, leaving out some of the less-used features. The receive part of the
|
|
76
81
|
code doesn't (yet) have the full capability of the advanced decoders used in WSJT-x, and so gets fewer decodes than WSJT-x gets, depending on band conditions (on a quiet band with only good signals PyFT8 will get close to 100%).
|
|
77
82
|
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
MANIFEST.in
|
|
3
|
+
README.md
|
|
4
|
+
pyproject.toml
|
|
5
|
+
PyFT8/__init__.py
|
|
6
|
+
PyFT8/gui.py
|
|
7
|
+
PyFT8/pyft8.py
|
|
8
|
+
PyFT8/receiver.py
|
|
9
|
+
PyFT8/rigctrl.py
|
|
10
|
+
PyFT8/time_utils.py
|
|
11
|
+
PyFT8/transmitter.py
|
|
12
|
+
PyFT8.egg-info/PKG-INFO
|
|
13
|
+
PyFT8.egg-info/SOURCES.txt
|
|
14
|
+
PyFT8.egg-info/dependency_links.txt
|
|
15
|
+
PyFT8.egg-info/entry_points.txt
|
|
16
|
+
PyFT8.egg-info/requires.txt
|
|
17
|
+
PyFT8.egg-info/top_level.txt
|
|
18
|
+
tests/plot_baseline.py
|
|
19
|
+
tests/spare.py
|
|
20
|
+
tests/test_batch_and_live.py
|
|
21
|
+
tests/dev/osd.py
|
|
22
|
+
tests/dev/test_generate_wav.py
|
|
23
|
+
tests/dev/test_loopback_performance.py
|
|
@@ -15,7 +15,8 @@ PyFT8 is somewhat experimental, with a focus on demonstrating FT8 written in Pyt
|
|
|
15
15
|
- Modern programming language throughout
|
|
16
16
|
- Finds sound cards by keywords so follows them if windows moves them ...
|
|
17
17
|
|
|
18
|
-
<img width="
|
|
18
|
+
<img width="1003" height="1020" alt="image" src="https://github.com/user-attachments/assets/bf6e3f78-531a-4c9b-ab2b-b51cc04ad980" />
|
|
19
|
+
|
|
19
20
|
|
|
20
21
|
|
|
21
22
|
## Motivation
|
|
@@ -42,7 +43,9 @@ Once installed, you can use the following commands to run it. Otherwise, please
|
|
|
42
43
|
| Launch configured GUI|pyft8 -i "Keyword1, Keyword2" -o "Keyword1, Keyword2" -c {config folder}| Config folder stores PyFT8.ini (your callsign, grid, buttons) and PyFT8.adi log file. Run this once to create default PyFT8.ini file.|
|
|
43
44
|
|
|
44
45
|
### Rig control
|
|
45
|
-
|
|
46
|
+
I'm resisting interfacing this to middleware such as Hamlib and Omnirig, because I want to keep the whole thing self-contained and simple, and losing connections to/from middleware was one motivation for writing this in the first place.
|
|
47
|
+
|
|
48
|
+
However, 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.
|
|
46
49
|
|
|
47
50
|
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.
|
|
48
51
|
|
|
@@ -50,9 +53,11 @@ Alternatively, you can run PyFT8 without rig control; if there is no rig found,
|
|
|
50
53
|
|
|
51
54
|
The image below shows the number of decodes from PyFT8, WSJT-x V2.7.0 running in NORM mode, and FT8_lib, using the same 10 minutes of busy 20m audio that is used to test ft8_lib.
|
|
52
55
|
|
|
53
|
-
<img width="
|
|
56
|
+
<img width="640" height="480" alt="performance snapshot" src="https://github.com/user-attachments/assets/fc84702a-4d76-475b-b7fe-489f4a09deed" />
|
|
57
|
+
|
|
54
58
|
|
|
55
59
|
## Limitations
|
|
60
|
+
|
|
56
61
|
In pursuit of tight code, I've concentrated on core standard messages, leaving out some of the less-used features. The receive part of the
|
|
57
62
|
code doesn't (yet) have the full capability of the advanced decoders used in WSJT-x, and so gets fewer decodes than WSJT-x gets, depending on band conditions (on a quiet band with only good signals PyFT8 will get close to 100%).
|
|
58
63
|
|
|
@@ -5,6 +5,7 @@ import pickle
|
|
|
5
5
|
import threading
|
|
6
6
|
from PyFT8.receiver import Receiver, AudioIn
|
|
7
7
|
from PyFT8.gui import Gui
|
|
8
|
+
from PyFT8.pyft8 import Message
|
|
8
9
|
|
|
9
10
|
class Wsjtx_all_tailer:
|
|
10
11
|
|
|
@@ -37,6 +38,7 @@ class Wsjtx_all_tailer:
|
|
|
37
38
|
|
|
38
39
|
|
|
39
40
|
data_folder = "C:/Users/drala/Documents/Projects/GitHub/PyFT8/tests/data/ft8_lib_20m_busy"
|
|
41
|
+
wav_folder = "C:/Users/drala/Documents/Projects/GitHub/ft8_lib/test/wav/20m_busy"
|
|
40
42
|
|
|
41
43
|
global decodes, py_times, ws_times, decodes
|
|
42
44
|
decodes, py_times, ws_times = [], [], []
|
|
@@ -52,11 +54,10 @@ def get_cumulative_from_text_files(i0, i1, postfix):
|
|
|
52
54
|
return times
|
|
53
55
|
|
|
54
56
|
def on_decode(c):
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
print(
|
|
59
|
-
decodes.append(decode_pack)
|
|
57
|
+
message = Message(c)
|
|
58
|
+
if gui:
|
|
59
|
+
gui.add_message_box(message)
|
|
60
|
+
print(message.wsjtx_screen_format())
|
|
60
61
|
py_times.append(time.time() - t_start)
|
|
61
62
|
|
|
62
63
|
def batch_test(i0, i1):
|
|
@@ -64,10 +65,10 @@ def batch_test(i0, i1):
|
|
|
64
65
|
global t_start, gui
|
|
65
66
|
wav_files = []
|
|
66
67
|
for idx in range(i0, i1):
|
|
67
|
-
wav_files.append(f"{
|
|
68
|
+
wav_files.append(f"{wav_folder}/test_{idx:02d}.wav")
|
|
68
69
|
audio_in = AudioIn(3100, wav_files)
|
|
69
|
-
gui = Gui(audio_in.dBgrid_main, 4, 2,
|
|
70
|
-
rx = Receiver(audio_in, [200, 3100], on_decode)
|
|
70
|
+
gui = Gui(audio_in.dBgrid_main, 4, 2, None, None, None)
|
|
71
|
+
rx = Receiver(audio_in, [200, 3100], on_decode, None)
|
|
71
72
|
audio_in.start_wav_load()
|
|
72
73
|
t_start = time.time()
|
|
73
74
|
with open('baseline.pkl', 'rb') as f:
|
|
@@ -104,8 +105,8 @@ def live_test():
|
|
|
104
105
|
from matplotlib.animation import FuncAnimation
|
|
105
106
|
global t_start, gui
|
|
106
107
|
audio_in = AudioIn(3100)
|
|
107
|
-
gui = Gui(audio_in.dBgrid_main, 4, 2,
|
|
108
|
-
rx = Receiver(audio_in, [200, 3100], on_decode)
|
|
108
|
+
gui = Gui(audio_in.dBgrid_main, 4, 2, config, None, None)
|
|
109
|
+
rx = Receiver(audio_in, [200, 3100], on_decode, None)
|
|
109
110
|
t_start = time.time()
|
|
110
111
|
input_device_idx = audio_in.find_device(["Cable", "Out"])
|
|
111
112
|
audio_in.start_streamed_audio(input_device_idx)
|
|
@@ -133,7 +134,6 @@ def live_test():
|
|
|
133
134
|
gui.plt.show()
|
|
134
135
|
|
|
135
136
|
#live_test()
|
|
136
|
-
|
|
137
137
|
batch_test(1,39)
|
|
138
138
|
|
|
139
139
|
|
pyft8-2.1.3/PyFT8/rigctrl.py
DELETED
|
@@ -1,67 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Modify this code to drive your particular rig.
|
|
3
|
-
|
|
4
|
-
This example is the code I use to control my IC-7100.
|
|
5
|
-
The changes you will need to make are the serial port and baud rate,
|
|
6
|
-
the internal functions (_xxxxx) if your rig is not Icom,
|
|
7
|
-
and the commands to set ptt on and off and to set frequency.
|
|
8
|
-
|
|
9
|
-
"""
|
|
10
|
-
|
|
11
|
-
class Rig:
|
|
12
|
-
import serial, time
|
|
13
|
-
|
|
14
|
-
def __init__(self, verbose = False, port = 'COM4', baudrate = 9600):
|
|
15
|
-
self.serial_port = False
|
|
16
|
-
self.port = port
|
|
17
|
-
self.baudrate = baudrate
|
|
18
|
-
self.verbose = verbose
|
|
19
|
-
|
|
20
|
-
def vprint(self, text):
|
|
21
|
-
if self.verbose:
|
|
22
|
-
print(text)
|
|
23
|
-
|
|
24
|
-
def connect(self):
|
|
25
|
-
try:
|
|
26
|
-
self.serial_port = self.serial.Serial(port = self.port, baudrate = self.baudrate, timeout = 0.1)
|
|
27
|
-
if (self.serial_port):
|
|
28
|
-
self.vprint(f"Connected to {self.port}")
|
|
29
|
-
except IOError:
|
|
30
|
-
print(f"Couldn't connect to {self.port}")
|
|
31
|
-
|
|
32
|
-
def _decode_twoBytes(self, twoBytes):
|
|
33
|
-
if(len(twoBytes)==2):
|
|
34
|
-
n1 = int(twoBytes[0])
|
|
35
|
-
n2 = int(twoBytes[1])
|
|
36
|
-
return n1*100 + (n2//16)*10 + n2 %16
|
|
37
|
-
|
|
38
|
-
def _sendCAT(self, cmd):
|
|
39
|
-
self.connect()
|
|
40
|
-
try:
|
|
41
|
-
self.serial_port.reset_input_buffer()
|
|
42
|
-
msg = b'\xfe\xfe\x88\xe0' + cmd + b'\xfd'
|
|
43
|
-
self.vprint(f"[CAT] send {msg.hex(' ')}")
|
|
44
|
-
self.serial_port.write(msg)
|
|
45
|
-
resp = self.serial_port.read_until(b'\xfd')
|
|
46
|
-
resp = self.serial_port.read_until(b'\xfd')
|
|
47
|
-
self.vprint(f"[CAT] response {resp.hex(' ')}")
|
|
48
|
-
self.serial_port.close()
|
|
49
|
-
return resp
|
|
50
|
-
except:
|
|
51
|
-
print("couldn't send command")
|
|
52
|
-
|
|
53
|
-
def PyFT8_set_freq_Hz(self, freqHz):
|
|
54
|
-
s = f"{freqHz:09d}"
|
|
55
|
-
self.vprint(f"[CAT] SET frequency")
|
|
56
|
-
self.vprint(f"[CAT] {s}")
|
|
57
|
-
fBytes = b"".join(bytes([b]) for b in [16*int(s[7])+int(s[8]),16*int(s[5])+int(s[6]),16*int(s[3])+int(s[4]),16*int(s[1])+int(s[2]), int(s[0])])
|
|
58
|
-
self._sendCAT(b"".join([b'\x00', fBytes]))
|
|
59
|
-
|
|
60
|
-
def PyFT8_ptt_on(self, PTT_on = b'\x1c\x00\x01'):
|
|
61
|
-
self.vprint(f"[CAT] PTT On")
|
|
62
|
-
self._sendCAT(PTT_on)
|
|
63
|
-
|
|
64
|
-
def PyFT8_ptt_off(self, PTT_off = b'\x1c\x00\x00'):
|
|
65
|
-
self.vprint(f"[CAT] PTT Off")
|
|
66
|
-
self._sendCAT(PTT_off)
|
|
67
|
-
|
|
@@ -1,61 +0,0 @@
|
|
|
1
|
-
LICENSE
|
|
2
|
-
MANIFEST.in
|
|
3
|
-
README.md
|
|
4
|
-
pyproject.toml
|
|
5
|
-
PyFT8/__init__.py
|
|
6
|
-
PyFT8/gui.py
|
|
7
|
-
PyFT8/pyft8.py
|
|
8
|
-
PyFT8/receiver.py
|
|
9
|
-
PyFT8/rigctrl.py
|
|
10
|
-
PyFT8/time_utils.py
|
|
11
|
-
PyFT8/transmitter.py
|
|
12
|
-
PyFT8.egg-info/PKG-INFO
|
|
13
|
-
PyFT8.egg-info/SOURCES.txt
|
|
14
|
-
PyFT8.egg-info/dependency_links.txt
|
|
15
|
-
PyFT8.egg-info/entry_points.txt
|
|
16
|
-
PyFT8.egg-info/requires.txt
|
|
17
|
-
PyFT8.egg-info/top_level.txt
|
|
18
|
-
tests/plot_baseline.py
|
|
19
|
-
tests/spare.py
|
|
20
|
-
tests/test_batch_and_live.py
|
|
21
|
-
tests/data/ft8_lib_20m_busy/test_01.wav
|
|
22
|
-
tests/data/ft8_lib_20m_busy/test_02.wav
|
|
23
|
-
tests/data/ft8_lib_20m_busy/test_03.wav
|
|
24
|
-
tests/data/ft8_lib_20m_busy/test_04.wav
|
|
25
|
-
tests/data/ft8_lib_20m_busy/test_05.wav
|
|
26
|
-
tests/data/ft8_lib_20m_busy/test_06.wav
|
|
27
|
-
tests/data/ft8_lib_20m_busy/test_07.wav
|
|
28
|
-
tests/data/ft8_lib_20m_busy/test_08.wav
|
|
29
|
-
tests/data/ft8_lib_20m_busy/test_09.wav
|
|
30
|
-
tests/data/ft8_lib_20m_busy/test_10.wav
|
|
31
|
-
tests/data/ft8_lib_20m_busy/test_11.wav
|
|
32
|
-
tests/data/ft8_lib_20m_busy/test_12.wav
|
|
33
|
-
tests/data/ft8_lib_20m_busy/test_13.wav
|
|
34
|
-
tests/data/ft8_lib_20m_busy/test_14.wav
|
|
35
|
-
tests/data/ft8_lib_20m_busy/test_15.wav
|
|
36
|
-
tests/data/ft8_lib_20m_busy/test_16.wav
|
|
37
|
-
tests/data/ft8_lib_20m_busy/test_17.wav
|
|
38
|
-
tests/data/ft8_lib_20m_busy/test_18.wav
|
|
39
|
-
tests/data/ft8_lib_20m_busy/test_19.wav
|
|
40
|
-
tests/data/ft8_lib_20m_busy/test_20.wav
|
|
41
|
-
tests/data/ft8_lib_20m_busy/test_21.wav
|
|
42
|
-
tests/data/ft8_lib_20m_busy/test_22.wav
|
|
43
|
-
tests/data/ft8_lib_20m_busy/test_23.wav
|
|
44
|
-
tests/data/ft8_lib_20m_busy/test_24.wav
|
|
45
|
-
tests/data/ft8_lib_20m_busy/test_25.wav
|
|
46
|
-
tests/data/ft8_lib_20m_busy/test_26.wav
|
|
47
|
-
tests/data/ft8_lib_20m_busy/test_27.wav
|
|
48
|
-
tests/data/ft8_lib_20m_busy/test_28.wav
|
|
49
|
-
tests/data/ft8_lib_20m_busy/test_29.wav
|
|
50
|
-
tests/data/ft8_lib_20m_busy/test_30.wav
|
|
51
|
-
tests/data/ft8_lib_20m_busy/test_31.wav
|
|
52
|
-
tests/data/ft8_lib_20m_busy/test_32.wav
|
|
53
|
-
tests/data/ft8_lib_20m_busy/test_33.wav
|
|
54
|
-
tests/data/ft8_lib_20m_busy/test_34.wav
|
|
55
|
-
tests/data/ft8_lib_20m_busy/test_35.wav
|
|
56
|
-
tests/data/ft8_lib_20m_busy/test_36.wav
|
|
57
|
-
tests/data/ft8_lib_20m_busy/test_37.wav
|
|
58
|
-
tests/data/ft8_lib_20m_busy/test_38.wav
|
|
59
|
-
tests/dev/osd.py
|
|
60
|
-
tests/dev/test_generate_wav.py
|
|
61
|
-
tests/dev/test_loopback_performance.py
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
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
|