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.
Files changed (65) hide show
  1. {pyft8-2.1.3 → pyft8-2.2.0}/PKG-INFO +9 -4
  2. {pyft8-2.1.3 → pyft8-2.2.0}/PyFT8/gui.py +52 -31
  3. {pyft8-2.1.3 → pyft8-2.2.0}/PyFT8/pyft8.py +41 -35
  4. {pyft8-2.1.3 → pyft8-2.2.0}/PyFT8/receiver.py +9 -1
  5. pyft8-2.2.0/PyFT8/rigctrl.py +72 -0
  6. {pyft8-2.1.3 → pyft8-2.2.0}/PyFT8.egg-info/PKG-INFO +9 -4
  7. pyft8-2.2.0/PyFT8.egg-info/SOURCES.txt +23 -0
  8. {pyft8-2.1.3 → pyft8-2.2.0}/README.md +8 -3
  9. {pyft8-2.1.3 → pyft8-2.2.0}/pyproject.toml +1 -1
  10. {pyft8-2.1.3 → pyft8-2.2.0}/tests/test_batch_and_live.py +11 -11
  11. pyft8-2.1.3/PyFT8/rigctrl.py +0 -67
  12. pyft8-2.1.3/PyFT8.egg-info/SOURCES.txt +0 -61
  13. pyft8-2.1.3/tests/data/ft8_lib_20m_busy/test_01.wav +0 -0
  14. pyft8-2.1.3/tests/data/ft8_lib_20m_busy/test_02.wav +0 -0
  15. pyft8-2.1.3/tests/data/ft8_lib_20m_busy/test_03.wav +0 -0
  16. pyft8-2.1.3/tests/data/ft8_lib_20m_busy/test_04.wav +0 -0
  17. pyft8-2.1.3/tests/data/ft8_lib_20m_busy/test_05.wav +0 -0
  18. pyft8-2.1.3/tests/data/ft8_lib_20m_busy/test_06.wav +0 -0
  19. pyft8-2.1.3/tests/data/ft8_lib_20m_busy/test_07.wav +0 -0
  20. pyft8-2.1.3/tests/data/ft8_lib_20m_busy/test_08.wav +0 -0
  21. pyft8-2.1.3/tests/data/ft8_lib_20m_busy/test_09.wav +0 -0
  22. pyft8-2.1.3/tests/data/ft8_lib_20m_busy/test_10.wav +0 -0
  23. pyft8-2.1.3/tests/data/ft8_lib_20m_busy/test_11.wav +0 -0
  24. pyft8-2.1.3/tests/data/ft8_lib_20m_busy/test_12.wav +0 -0
  25. pyft8-2.1.3/tests/data/ft8_lib_20m_busy/test_13.wav +0 -0
  26. pyft8-2.1.3/tests/data/ft8_lib_20m_busy/test_14.wav +0 -0
  27. pyft8-2.1.3/tests/data/ft8_lib_20m_busy/test_15.wav +0 -0
  28. pyft8-2.1.3/tests/data/ft8_lib_20m_busy/test_16.wav +0 -0
  29. pyft8-2.1.3/tests/data/ft8_lib_20m_busy/test_17.wav +0 -0
  30. pyft8-2.1.3/tests/data/ft8_lib_20m_busy/test_18.wav +0 -0
  31. pyft8-2.1.3/tests/data/ft8_lib_20m_busy/test_19.wav +0 -0
  32. pyft8-2.1.3/tests/data/ft8_lib_20m_busy/test_20.wav +0 -0
  33. pyft8-2.1.3/tests/data/ft8_lib_20m_busy/test_21.wav +0 -0
  34. pyft8-2.1.3/tests/data/ft8_lib_20m_busy/test_22.wav +0 -0
  35. pyft8-2.1.3/tests/data/ft8_lib_20m_busy/test_23.wav +0 -0
  36. pyft8-2.1.3/tests/data/ft8_lib_20m_busy/test_24.wav +0 -0
  37. pyft8-2.1.3/tests/data/ft8_lib_20m_busy/test_25.wav +0 -0
  38. pyft8-2.1.3/tests/data/ft8_lib_20m_busy/test_26.wav +0 -0
  39. pyft8-2.1.3/tests/data/ft8_lib_20m_busy/test_27.wav +0 -0
  40. pyft8-2.1.3/tests/data/ft8_lib_20m_busy/test_28.wav +0 -0
  41. pyft8-2.1.3/tests/data/ft8_lib_20m_busy/test_29.wav +0 -0
  42. pyft8-2.1.3/tests/data/ft8_lib_20m_busy/test_30.wav +0 -0
  43. pyft8-2.1.3/tests/data/ft8_lib_20m_busy/test_31.wav +0 -0
  44. pyft8-2.1.3/tests/data/ft8_lib_20m_busy/test_32.wav +0 -0
  45. pyft8-2.1.3/tests/data/ft8_lib_20m_busy/test_33.wav +0 -0
  46. pyft8-2.1.3/tests/data/ft8_lib_20m_busy/test_34.wav +0 -0
  47. pyft8-2.1.3/tests/data/ft8_lib_20m_busy/test_35.wav +0 -0
  48. pyft8-2.1.3/tests/data/ft8_lib_20m_busy/test_36.wav +0 -0
  49. pyft8-2.1.3/tests/data/ft8_lib_20m_busy/test_37.wav +0 -0
  50. pyft8-2.1.3/tests/data/ft8_lib_20m_busy/test_38.wav +0 -0
  51. {pyft8-2.1.3 → pyft8-2.2.0}/LICENSE +0 -0
  52. {pyft8-2.1.3 → pyft8-2.2.0}/MANIFEST.in +0 -0
  53. {pyft8-2.1.3 → pyft8-2.2.0}/PyFT8/__init__.py +0 -0
  54. {pyft8-2.1.3 → pyft8-2.2.0}/PyFT8/time_utils.py +0 -0
  55. {pyft8-2.1.3 → pyft8-2.2.0}/PyFT8/transmitter.py +0 -0
  56. {pyft8-2.1.3 → pyft8-2.2.0}/PyFT8.egg-info/dependency_links.txt +0 -0
  57. {pyft8-2.1.3 → pyft8-2.2.0}/PyFT8.egg-info/entry_points.txt +0 -0
  58. {pyft8-2.1.3 → pyft8-2.2.0}/PyFT8.egg-info/requires.txt +0 -0
  59. {pyft8-2.1.3 → pyft8-2.2.0}/PyFT8.egg-info/top_level.txt +0 -0
  60. {pyft8-2.1.3 → pyft8-2.2.0}/setup.cfg +0 -0
  61. {pyft8-2.1.3 → pyft8-2.2.0}/tests/dev/osd.py +0 -0
  62. {pyft8-2.1.3 → pyft8-2.2.0}/tests/dev/test_generate_wav.py +0 -0
  63. {pyft8-2.1.3 → pyft8-2.2.0}/tests/dev/test_loopback_performance.py +0 -0
  64. {pyft8-2.1.3 → pyft8-2.2.0}/tests/plot_baseline.py +0 -0
  65. {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.1.3
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="883" height="1001" alt="image" src="https://github.com/user-attachments/assets/7a93560e-2f3c-4d8b-ac5a-3db97329bf36" />
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
- PyFT8 doesn't currently support CAT control for rigs in general. However, I've included the Python code that I use with my Icom IC-7100 in the file 'rigctrl.py'. You can modify this to control your own rig of course if you know Python.
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="844" height="562" alt="performance snapshot" src="https://github.com/user-attachments/assets/08ba1946-b816-448f-9510-7763a2f065bd" />
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
- self.mStation = {'c':config['station']['call'], 'g':config['station']['grid']}
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.simple_message_art = None
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 simple_message(self, text, color):
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.tight_layout()
72
- self.image = self.ax_wf.imshow(self.dBgrid.T,vmax=120,vmin=90,origin='lower',interpolation='none')
73
- wf_ylim = self.ax_wf.get_ylim()
74
- self.ax_wf.set_axis_off()
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
- styles = {'ctrl':{'fc':'grey','c':'black'}, 'band':{'fc':'green','c':'white'}}
78
- control_buttons = [{'label':'CQ','style':'ctrl','data':None}, {'label':'Repeat last','style':'ctrl','data':None},
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[-1], btn['label'], color=style['fc'], hovercolor='skyblue')
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
- self.ani = FuncAnimation(self.fig, self._animate, interval = 40, frames=(100000), blit=True)
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
- print(f"Wrote default config to {ini_file}")
37
- print(f"Reading config from {ini_file}")
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
- print(f"Logged QSO with {oStation['c']}")
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 = qso.mStation['c']
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
- self.mStation = {'c':config['station']['call'], 'g':config['station']['grid']}
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
- gui.simple_message("Please select a band before transmitting", color = 'red')
139
+ console_print("[PyFT8] Please select a band before transmitting", color = 'red')
143
140
  return
144
- print(f"[QSO] Set transmit message to '{message}' with tx cycle = {self.tx_cycle}")
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
- print("No output device")
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
- print(f"Transmitting {self.message_to_transmit} on cycle {self.tx_cycle}")
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.PyFT8_ptt_on()
162
+ rig.ptt_on()
166
163
  audio_out.play_data_to_soundcard(audio_data, output_device_idx)
167
- rig.PyFT8_ptt_off()
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.log(self.times, self.band_info, self.mStation, self.oStation, self.rpts)
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
- print("Try next cycle")
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
- print(f"Created wave file {wave_output_file}")
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
- print(f"[on_busy] Set Tx freq to {clear_frequencies[cycle]:6.1f} for cycle {cycle}")
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
- rig.PyFT8_ptt_off()
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.PyFT8_set_freq_Hz(int(1000000*float(qso.band_info['f'])))
262
- gui.simple_message(f"{qso.band_info['b']} {qso.band_info['f']}", 'black')
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 = load_rigctrl()
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
- print("No input device")
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 = ' '.join(self.msg_tuple)
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.1.3
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="883" height="1001" alt="image" src="https://github.com/user-attachments/assets/7a93560e-2f3c-4d8b-ac5a-3db97329bf36" />
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
- PyFT8 doesn't currently support CAT control for rigs in general. However, I've included the Python code that I use with my Icom IC-7100 in the file 'rigctrl.py'. You can modify this to control your own rig of course if you know Python.
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="844" height="562" alt="performance snapshot" src="https://github.com/user-attachments/assets/08ba1946-b816-448f-9510-7763a2f065bd" />
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="883" height="1001" alt="image" src="https://github.com/user-attachments/assets/7a93560e-2f3c-4d8b-ac5a-3db97329bf36" />
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
- PyFT8 doesn't currently support CAT control for rigs in general. However, I've included the Python code that I use with my Icom IC-7100 in the file 'rigctrl.py'. You can modify this to control your own rig of course if you know Python.
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="844" height="562" alt="performance snapshot" src="https://github.com/user-attachments/assets/08ba1946-b816-448f-9510-7763a2f065bd" />
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
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "PyFT8"
3
- version = "2.1.3"
3
+ version = "2.2.0"
4
4
  license = "GPL-3.0-or-later"
5
5
 
6
6
  authors = [
@@ -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
- global decodes, py_times
56
- decode_pack = (c.h0_idx, c.f0_idx, c.msg, int(c.snr))
57
- gui.post_decode(decode_pack)
58
- print(f"{c.cyclestart_str} {c.snr} {c.dt:4.1f} {c.fHz} ~ {c.msg}")
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"{data_folder}/test_{idx:02d}.wav")
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, {'c':'xxx', 'g':''}, None, None)
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, {'c':'xxx', 'g':''}, None, None)
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
 
@@ -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
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