PyFT8 2.1.3__tar.gz → 2.1.4__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 (64) hide show
  1. {pyft8-2.1.3 → pyft8-2.1.4}/PKG-INFO +7 -4
  2. {pyft8-2.1.3 → pyft8-2.1.4}/PyFT8/gui.py +53 -30
  3. {pyft8-2.1.3 → pyft8-2.1.4}/PyFT8/pyft8.py +34 -23
  4. {pyft8-2.1.3 → pyft8-2.1.4}/PyFT8/receiver.py +9 -1
  5. {pyft8-2.1.3 → pyft8-2.1.4}/PyFT8.egg-info/PKG-INFO +7 -4
  6. pyft8-2.1.4/PyFT8.egg-info/SOURCES.txt +23 -0
  7. {pyft8-2.1.3 → pyft8-2.1.4}/README.md +6 -3
  8. {pyft8-2.1.3 → pyft8-2.1.4}/pyproject.toml +1 -1
  9. {pyft8-2.1.3 → pyft8-2.1.4}/tests/test_batch_and_live.py +11 -11
  10. pyft8-2.1.3/PyFT8.egg-info/SOURCES.txt +0 -61
  11. pyft8-2.1.3/tests/data/ft8_lib_20m_busy/test_01.wav +0 -0
  12. pyft8-2.1.3/tests/data/ft8_lib_20m_busy/test_02.wav +0 -0
  13. pyft8-2.1.3/tests/data/ft8_lib_20m_busy/test_03.wav +0 -0
  14. pyft8-2.1.3/tests/data/ft8_lib_20m_busy/test_04.wav +0 -0
  15. pyft8-2.1.3/tests/data/ft8_lib_20m_busy/test_05.wav +0 -0
  16. pyft8-2.1.3/tests/data/ft8_lib_20m_busy/test_06.wav +0 -0
  17. pyft8-2.1.3/tests/data/ft8_lib_20m_busy/test_07.wav +0 -0
  18. pyft8-2.1.3/tests/data/ft8_lib_20m_busy/test_08.wav +0 -0
  19. pyft8-2.1.3/tests/data/ft8_lib_20m_busy/test_09.wav +0 -0
  20. pyft8-2.1.3/tests/data/ft8_lib_20m_busy/test_10.wav +0 -0
  21. pyft8-2.1.3/tests/data/ft8_lib_20m_busy/test_11.wav +0 -0
  22. pyft8-2.1.3/tests/data/ft8_lib_20m_busy/test_12.wav +0 -0
  23. pyft8-2.1.3/tests/data/ft8_lib_20m_busy/test_13.wav +0 -0
  24. pyft8-2.1.3/tests/data/ft8_lib_20m_busy/test_14.wav +0 -0
  25. pyft8-2.1.3/tests/data/ft8_lib_20m_busy/test_15.wav +0 -0
  26. pyft8-2.1.3/tests/data/ft8_lib_20m_busy/test_16.wav +0 -0
  27. pyft8-2.1.3/tests/data/ft8_lib_20m_busy/test_17.wav +0 -0
  28. pyft8-2.1.3/tests/data/ft8_lib_20m_busy/test_18.wav +0 -0
  29. pyft8-2.1.3/tests/data/ft8_lib_20m_busy/test_19.wav +0 -0
  30. pyft8-2.1.3/tests/data/ft8_lib_20m_busy/test_20.wav +0 -0
  31. pyft8-2.1.3/tests/data/ft8_lib_20m_busy/test_21.wav +0 -0
  32. pyft8-2.1.3/tests/data/ft8_lib_20m_busy/test_22.wav +0 -0
  33. pyft8-2.1.3/tests/data/ft8_lib_20m_busy/test_23.wav +0 -0
  34. pyft8-2.1.3/tests/data/ft8_lib_20m_busy/test_24.wav +0 -0
  35. pyft8-2.1.3/tests/data/ft8_lib_20m_busy/test_25.wav +0 -0
  36. pyft8-2.1.3/tests/data/ft8_lib_20m_busy/test_26.wav +0 -0
  37. pyft8-2.1.3/tests/data/ft8_lib_20m_busy/test_27.wav +0 -0
  38. pyft8-2.1.3/tests/data/ft8_lib_20m_busy/test_28.wav +0 -0
  39. pyft8-2.1.3/tests/data/ft8_lib_20m_busy/test_29.wav +0 -0
  40. pyft8-2.1.3/tests/data/ft8_lib_20m_busy/test_30.wav +0 -0
  41. pyft8-2.1.3/tests/data/ft8_lib_20m_busy/test_31.wav +0 -0
  42. pyft8-2.1.3/tests/data/ft8_lib_20m_busy/test_32.wav +0 -0
  43. pyft8-2.1.3/tests/data/ft8_lib_20m_busy/test_33.wav +0 -0
  44. pyft8-2.1.3/tests/data/ft8_lib_20m_busy/test_34.wav +0 -0
  45. pyft8-2.1.3/tests/data/ft8_lib_20m_busy/test_35.wav +0 -0
  46. pyft8-2.1.3/tests/data/ft8_lib_20m_busy/test_36.wav +0 -0
  47. pyft8-2.1.3/tests/data/ft8_lib_20m_busy/test_37.wav +0 -0
  48. pyft8-2.1.3/tests/data/ft8_lib_20m_busy/test_38.wav +0 -0
  49. {pyft8-2.1.3 → pyft8-2.1.4}/LICENSE +0 -0
  50. {pyft8-2.1.3 → pyft8-2.1.4}/MANIFEST.in +0 -0
  51. {pyft8-2.1.3 → pyft8-2.1.4}/PyFT8/__init__.py +0 -0
  52. {pyft8-2.1.3 → pyft8-2.1.4}/PyFT8/rigctrl.py +0 -0
  53. {pyft8-2.1.3 → pyft8-2.1.4}/PyFT8/time_utils.py +0 -0
  54. {pyft8-2.1.3 → pyft8-2.1.4}/PyFT8/transmitter.py +0 -0
  55. {pyft8-2.1.3 → pyft8-2.1.4}/PyFT8.egg-info/dependency_links.txt +0 -0
  56. {pyft8-2.1.3 → pyft8-2.1.4}/PyFT8.egg-info/entry_points.txt +0 -0
  57. {pyft8-2.1.3 → pyft8-2.1.4}/PyFT8.egg-info/requires.txt +0 -0
  58. {pyft8-2.1.3 → pyft8-2.1.4}/PyFT8.egg-info/top_level.txt +0 -0
  59. {pyft8-2.1.3 → pyft8-2.1.4}/setup.cfg +0 -0
  60. {pyft8-2.1.3 → pyft8-2.1.4}/tests/dev/osd.py +0 -0
  61. {pyft8-2.1.3 → pyft8-2.1.4}/tests/dev/test_generate_wav.py +0 -0
  62. {pyft8-2.1.3 → pyft8-2.1.4}/tests/dev/test_loopback_performance.py +0 -0
  63. {pyft8-2.1.3 → pyft8-2.1.4}/tests/plot_baseline.py +0 -0
  64. {pyft8-2.1.3 → pyft8-2.1.4}/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.1.4
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
@@ -69,9 +70,11 @@ Alternatively, you can run PyFT8 without rig control; if there is no rig found,
69
70
 
70
71
  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
72
 
72
- <img width="844" height="562" alt="performance snapshot" src="https://github.com/user-attachments/assets/08ba1946-b816-448f-9510-7763a2f065bd" />
73
+ <img width="640" height="480" alt="performance snapshot" src="https://github.com/user-attachments/assets/fc84702a-4d76-475b-b7fe-489f4a09deed" />
74
+
75
+
76
+ ## Limitations![Uploading performance snapshot.png…]()
73
77
 
74
- ## Limitations
75
78
  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
79
  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
80
 
@@ -8,6 +8,31 @@ 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.clear()
20
+
21
+ def clear(self):
22
+ self.ax.cla()
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.clear()
29
+ self.lines = self.lines[-(self.nlines-1):]
30
+ self.lines.append({'art':None, 'text':text, 'color':color})
31
+ for i, line in enumerate(self.lines):
32
+ if line['text'] is not None:
33
+ line['art'] = self.ax.text(0.03,1 - self.line_height * (i+1), line['text'], color = line['color'], fontsize = self.fontsize)
34
+ self.fig.canvas.draw()
35
+
11
36
  class Msg_box:
12
37
  def __init__(self, fig, ax, tbin, fbin, w, h, onclick):
13
38
  from matplotlib.patches import Rectangle
@@ -47,50 +72,48 @@ class Msg_box:
47
72
 
48
73
  class Gui:
49
74
  def __init__(self, dBgrid, hps, bpt, config, on_msg_click, on_control_click):
50
- self.mStation = {'c':config['station']['call'], 'g':config['station']['grid']}
75
+ if config is not None:
76
+ self.mStation = {'c':config['station']['call'], 'g':config['station']['grid']}
51
77
  self.on_msg_click = on_msg_click
52
78
  self.on_control_click = on_control_click
53
79
  self.dBgrid = dBgrid
54
80
  self.hps, self.bpt = hps, bpt
55
81
  self.msg_boxes = {}
56
82
  self.decode_queue = queue.Queue()
57
- self.simple_message_art = None
83
+ self.pmarg = 0.04
58
84
  self.make_layout(config)
85
+ self.ani = FuncAnimation(self.fig, self._animate, interval = 40, frames=(100000), blit=True)
59
86
 
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')
87
+ def make_layout(self, config, wf_left = 0.15, wf_top = 0.87):
70
88
  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
-
89
+ self.fig = plt.figure(figsize = (10,10), facecolor=(.18, .71, .71, 0.4))
90
+ self.fig.canvas.manager.set_window_title('PyFT8 by G1OJS')
91
+ self.ax_wf = self.fig.add_axes([self.pmarg + wf_left, self.pmarg, 1-2*self.pmarg-wf_left, wf_top-self.pmarg])
92
+ self.image = self.ax_wf.imshow(self.dBgrid.T,vmax=120,vmin=90,origin='lower',interpolation='none', aspect = 'auto')
93
+ self.ax_wf.set_xticks([])
94
+ self.ax_wf.set_yticks([])
95
+ self.ax_console = self.fig.add_axes([self.pmarg + wf_left, wf_top, 1-2*self.pmarg - wf_left, 1-self.pmarg-wf_top])
96
+ self.console = Scrollbox(self.fig, self.ax_console)
97
+
98
+ if config is not None:
99
+ styles = {'ctrl':{'fc':'grey','c':'black'}, 'band':{'fc':'green','c':'white'}}
100
+ button_defs = [{'label':'CQ','style':'ctrl','data':None}, {'label':'Repeat last','style':'ctrl','data':None},
101
+ {'label':'Tx off','style':'ctrl','data':None}]
102
+ #{'label':'Averaging','style':'ctrl','data':None}]
103
+ for band, freq in config['bands'].items():
104
+ button_defs.append({'label':band,'style':'band','data':freq})
105
+ self._make_buttons(button_defs, styles, wf_top, 0.02, 0.1, 0.002)
106
+
107
+ def _make_buttons(self, buttons, styles, btns_top, btn_h, btn_w, sep_h):
76
108
  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]))
109
+ for i, btn in enumerate(buttons):
110
+ btn_axs = plt.axes([self.pmarg, btns_top - (i+1) * btn_h, btn_w, btn_h-sep_h])
87
111
  style = styles[btn['style']]
88
- btn_widg = Button(btn_axs[-1], btn['label'], color=style['fc'], hovercolor='skyblue')
112
+ btn_widg = Button(btn_axs, btn['label'], color=style['fc'], hovercolor='skyblue')
89
113
  btn_widg.data = btn['data']
90
114
  btn_widg.on_clicked(lambda event, btn_widg=btn_widg: self.on_control_click(btn_widg))
91
115
  self.buttons.append(btn_widg)
92
- self.ani = FuncAnimation(self.fig, self._animate, interval = 40, frames=(100000), blit=True)
93
-
116
+
94
117
  def add_message_box(self, message):
95
118
  self.decode_queue.put(message)
96
119
 
@@ -11,16 +11,15 @@ from PyFT8.time_utils import global_time_utils
11
11
 
12
12
  MAX_TX_START_SECONDS = 2.5
13
13
  T_CYC = 15
14
- rig = None
15
- gui = None
14
+ rig, gui, qso, worked_before = None, None, None, None
16
15
 
17
16
  def load_rigctrl():
18
17
  try:
19
18
  from PyFT8.rigctrl import Rig
20
- print("Loaded Rig control")
19
+ console_print("Loaded Rig control")
21
20
  return Rig()
22
21
  except ImportError:
23
- print("No Rig control found")
22
+ console_print("No Rig control found")
24
23
  return None
25
24
 
26
25
  def get_config(config_folder):
@@ -33,8 +32,8 @@ def get_config(config_folder):
33
32
  config['bands'] = {'20m':14.074}
34
33
  with open(ini_file, 'w') as f:
35
34
  config.write(f)
36
- print(f"Wrote default config to {ini_file}")
37
- print(f"Reading config from {ini_file}")
35
+ console_print(f"Wrote default config to {ini_file}")
36
+ console_print(f"Reading config from {ini_file}")
38
37
  config.read(ini_file)
39
38
 
40
39
  def parse_from_adif_rec(rec, field):
@@ -97,13 +96,15 @@ class Logging:
97
96
  f.write(f"<{k}:{len(v)}>{v} ")
98
97
  f.write(f"<eor>\n")
99
98
  self.update_worked_before(oStation['c'], time.time())
100
- print(f"Logged QSO with {oStation['c']}")
99
+ console_print(f"Logged QSO with {oStation['c']}")
101
100
 
102
101
 
103
102
  class Message:
104
103
  def __init__(self, candidate):
105
104
  c = candidate
106
- mycall = qso.mStation['c']
105
+ mycall = ''
106
+ if qso is not None:
107
+ mycall = qso.mStation['c']
107
108
  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
109
  self.cyclestart = c.cyclestart
109
110
  self.expire = time.time() + 29.8
@@ -111,7 +112,7 @@ class Message:
111
112
  self.is_to_me = c.msg_tuple[0] == mycall
112
113
  self.is_cq = c.msg_tuple[0].startswith('CQ')
113
114
  gui_wb_text = ''
114
- if self.is_cq:
115
+ if self.is_cq and worked_before is not None:
115
116
  if c.msg_tuple[1] in worked_before:
116
117
  gui_wb_text = f"wb: {global_time_utils.format_duration(time.time() - worked_before[c.msg_tuple[1]])}"
117
118
  self.gui_text = f"{c.msg} {gui_wb_text}"
@@ -123,7 +124,8 @@ class Message:
123
124
  class FT8_QSO:
124
125
  def __init__(self, logging):
125
126
  self.logging = logging
126
- self.mStation = {'c':config['station']['call'], 'g':config['station']['grid']}
127
+ if config is not None:
128
+ self.mStation = {'c':config['station']['call'], 'g':config['station']['grid']}
127
129
  self.band_info = {'b':None, 'f':0}
128
130
  self.tx_freq = 750
129
131
  threading.Thread(target = self._transmitter, daemon = True).start()
@@ -139,9 +141,9 @@ class FT8_QSO:
139
141
 
140
142
  def set_tx_message(self, message):
141
143
  if gui and self.band_info['b'] is None:
142
- gui.simple_message("Please select a band before transmitting", color = 'red')
144
+ console_print("[PyFT8] Please select a band before transmitting", color = 'red')
143
145
  return
144
- print(f"[QSO] Set transmit message to '{message}' with tx cycle = {self.tx_cycle}")
146
+ console_print(f"[QSO] Set transmit message to '{message}' with tx cycle = {self.tx_cycle}")
145
147
  self.message_to_transmit = message
146
148
 
147
149
  def _transmitter(self):
@@ -150,7 +152,7 @@ class FT8_QSO:
150
152
  if self.message_to_transmit is None:
151
153
  continue
152
154
  if output_device_idx is None:
153
- print("No output device")
155
+ console_print("No output device")
154
156
  return
155
157
  ct = global_time_utils.cycle_time()
156
158
  if ct > MAX_TX_START_SECONDS:
@@ -159,7 +161,7 @@ class FT8_QSO:
159
161
  if self.tx_cycle is None:
160
162
  self.tx_cycle = global_time_utils.curr_cycle_from_time()
161
163
  self.tx_freq = clear_frequencies[self.tx_cycle]
162
- print(f"Transmitting {self.message_to_transmit} on cycle {self.tx_cycle}")
164
+ console_print(f"Transmitting {self.message_to_transmit} on cycle {self.tx_cycle}")
163
165
  symbols = audio_out.create_ft8_symbols(self.message_to_transmit)
164
166
  audio_data = audio_out.create_ft8_wave(symbols, f_base = self.tx_freq)
165
167
  rig.PyFT8_ptt_on()
@@ -169,7 +171,8 @@ class FT8_QSO:
169
171
  self.message_to_transmit = None
170
172
 
171
173
  def log(self):
172
- self.logging.log(self.times, self.band_info, self.mStation, self.oStation, self.rpts)
174
+ if self.logging is not None:
175
+ self.logging.log(self.times, self.band_info, self.mStation, self.oStation, self.rpts)
173
176
 
174
177
  def isReport(grid_rpt): return "+" in grid_rpt or "-" in grid_rpt
175
178
  def isRReport(grid_rpt): return isReport(grid_rpt) and 'R' in grid_rpt
@@ -182,12 +185,14 @@ def progress_qso(clicked_message):
182
185
  global qso
183
186
 
184
187
  if time.time() - clicked_message.cyclestart['time'] > (15 + MAX_TX_START_SECONDS):
185
- print("Try next cycle")
188
+ console_print("Try next cycle")
186
189
  return
187
190
 
188
191
  call_a, call_b, grid_rpt = clicked_message.msg_tuple
189
192
  my_station = qso.mStation
190
193
  reply = ""
194
+ msg = ' '.join(clicked_message.msg_tuple)
195
+ console_print(f"[QSO] Clicked on message '{msg}'")
191
196
 
192
197
  if call_a == "CQ":
193
198
  qso.clear()
@@ -212,7 +217,7 @@ def progress_qso(clicked_message):
212
217
  if isRR73(grid_rpt):
213
218
  reply = f"{qso.oStation['c']} {my_station['c']} 73"
214
219
  qso.set_tx_message(reply)
215
-
220
+
216
221
  if is73(grid_rpt) or " 73" in reply or isRR73(grid_rpt):
217
222
  qso.times['time_off'] = time.gmtime()
218
223
  qso.log()
@@ -221,7 +226,7 @@ def make_wav(msg, wave_output_file): # move to transmitter.py?
221
226
  symbols = audio_out.create_ft8_symbols(msg)
222
227
  audio_data = audio_out.create_ft8_wave(symbols)
223
228
  audio_out.write_to_wave_file(audio_data, wave_output_file)
224
- print(f"Created wave file {wave_output_file}")
229
+ console_print(f"Created wave file {wave_output_file}")
225
230
 
226
231
  def wait_for_keyboard():
227
232
  import time
@@ -245,7 +250,7 @@ def on_busy_profile(busy_profile, cycle):
245
250
  f0_idx, fn_idx = int(500/audio_in.df), int(fmax/audio_in.df)
246
251
  idx = np.argmin(busy_profile[f0_idx:fn_idx])
247
252
  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}")
253
+ console_print(f"[on_busy] Set Tx freq to {clear_frequencies[cycle]:6.1f} for cycle {cycle}")
249
254
 
250
255
  def on_control_click(btn_widg):
251
256
  btn_text, btn_data = btn_widg.label.get_text(), btn_widg.data
@@ -255,16 +260,23 @@ def on_control_click(btn_widg):
255
260
  if btn_text == "Repeat last":
256
261
  qso.set_tx_message(qso.last_tx)
257
262
  if btn_text == "Tx off":
263
+ console_print("[PyFT8] Set PTT Off")
258
264
  rig.PyFT8_ptt_off()
259
265
  if('m' in btn_text):
260
266
  qso.band_info = {'b':btn_text, 'f':btn_data}
261
267
  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')
268
+ console_print(f"[PyFT8] Set band: {qso.band_info['b']} {qso.band_info['f']}")
263
269
 
264
270
  def on_msg_click(message):
265
271
  progress_qso(message)
266
272
 
267
- #=============== CLI ========================================================================
273
+ #=============== CLI ========================================================================
274
+ def console_print(text, color = 'white'):
275
+ if gui is not None:
276
+ gui.console.print(text, color)
277
+ else:
278
+ print(text)
279
+
268
280
  def cli():
269
281
  global audio_in, audio_out, output_device_idx, rig, gui, qso, config, clear_frequencies
270
282
  import time
@@ -282,7 +294,6 @@ def cli():
282
294
  config_folder = f"{args.config_folder}".strip()
283
295
  get_config(config_folder)
284
296
  logging = Logging(config_folder)
285
- #print(worked_before)
286
297
  qso = FT8_QSO(logging)
287
298
  rig = load_rigctrl()
288
299
 
@@ -303,7 +314,7 @@ def cli():
303
314
  audio_in = AudioIn(3100)
304
315
  input_device_idx = audio_in.find_device(args.inputcard_keywords.replace(' ','').split(','))
305
316
  if not input_device_idx:
306
- print("No input device")
317
+ console_print("No input device")
307
318
  else:
308
319
  gui = None if args.no_gui else Gui(audio_in.dBgrid_main, 4, 2, config, on_msg_click, on_control_click)
309
320
  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
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: PyFT8
3
- Version: 2.1.3
3
+ Version: 2.1.4
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
@@ -69,9 +70,11 @@ Alternatively, you can run PyFT8 without rig control; if there is no rig found,
69
70
 
70
71
  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
72
 
72
- <img width="844" height="562" alt="performance snapshot" src="https://github.com/user-attachments/assets/08ba1946-b816-448f-9510-7763a2f065bd" />
73
+ <img width="640" height="480" alt="performance snapshot" src="https://github.com/user-attachments/assets/fc84702a-4d76-475b-b7fe-489f4a09deed" />
74
+
75
+
76
+ ## Limitations![Uploading performance snapshot.png…]()
73
77
 
74
- ## Limitations
75
78
  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
79
  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
80
 
@@ -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
@@ -50,9 +51,11 @@ Alternatively, you can run PyFT8 without rig control; if there is no rig found,
50
51
 
51
52
  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
53
 
53
- <img width="844" height="562" alt="performance snapshot" src="https://github.com/user-attachments/assets/08ba1946-b816-448f-9510-7763a2f065bd" />
54
+ <img width="640" height="480" alt="performance snapshot" src="https://github.com/user-attachments/assets/fc84702a-4d76-475b-b7fe-489f4a09deed" />
55
+
56
+
57
+ ## Limitations![Uploading performance snapshot.png…]()
54
58
 
55
- ## Limitations
56
59
  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
60
  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
61
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "PyFT8"
3
- version = "2.1.3"
3
+ version = "2.1.4"
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,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
File without changes