PyFT8 3.3.1__tar.gz → 3.6.1__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 (43) hide show
  1. {pyft8-3.3.1 → pyft8-3.6.1}/PKG-INFO +2 -3
  2. {pyft8-3.3.1 → pyft8-3.6.1}/PyFT8/databases.py +35 -14
  3. pyft8-3.6.1/PyFT8/gui.py +298 -0
  4. pyft8-3.6.1/PyFT8/message_broker.py +70 -0
  5. pyft8-3.6.1/PyFT8/pyft8.py +169 -0
  6. pyft8-3.6.1/PyFT8/qso_manager.py +122 -0
  7. {pyft8-3.3.1 → pyft8-3.6.1}/PyFT8/receiver.py +46 -41
  8. {pyft8-3.3.1 → pyft8-3.6.1}/PyFT8.egg-info/PKG-INFO +2 -3
  9. {pyft8-3.3.1 → pyft8-3.6.1}/PyFT8.egg-info/SOURCES.txt +2 -0
  10. {pyft8-3.3.1 → pyft8-3.6.1}/README.md +1 -2
  11. {pyft8-3.3.1 → pyft8-3.6.1}/pyproject.toml +1 -1
  12. pyft8-3.6.1/tests/misc/text.py +10 -0
  13. {pyft8-3.3.1 → pyft8-3.6.1}/tests/test_batch_and_live.py +10 -11
  14. pyft8-3.3.1/PyFT8/gui.py +0 -310
  15. pyft8-3.3.1/PyFT8/pyft8.py +0 -219
  16. pyft8-3.3.1/PyFT8/qso_manager.py +0 -119
  17. {pyft8-3.3.1 → pyft8-3.6.1}/LICENSE +0 -0
  18. {pyft8-3.3.1 → pyft8-3.6.1}/MANIFEST.in +0 -0
  19. {pyft8-3.3.1 → pyft8-3.6.1}/PyFT8/__init__.py +0 -0
  20. {pyft8-3.3.1 → pyft8-3.6.1}/PyFT8/osd.py +0 -0
  21. {pyft8-3.3.1 → pyft8-3.6.1}/PyFT8/pskreporter.py +0 -0
  22. {pyft8-3.3.1 → pyft8-3.6.1}/PyFT8/rigctrl.py +0 -0
  23. {pyft8-3.3.1 → pyft8-3.6.1}/PyFT8/time_utils.py +0 -0
  24. {pyft8-3.3.1 → pyft8-3.6.1}/PyFT8/transmitter.py +0 -0
  25. {pyft8-3.3.1 → pyft8-3.6.1}/PyFT8.egg-info/dependency_links.txt +0 -0
  26. {pyft8-3.3.1 → pyft8-3.6.1}/PyFT8.egg-info/entry_points.txt +0 -0
  27. {pyft8-3.3.1 → pyft8-3.6.1}/PyFT8.egg-info/requires.txt +0 -0
  28. {pyft8-3.3.1 → pyft8-3.6.1}/PyFT8.egg-info/top_level.txt +0 -0
  29. {pyft8-3.3.1 → pyft8-3.6.1}/setup.cfg +0 -0
  30. {pyft8-3.3.1 → pyft8-3.6.1}/tests/misc/CQ AAAA.py +0 -0
  31. {pyft8-3.3.1 → pyft8-3.6.1}/tests/misc/animation.py +0 -0
  32. {pyft8-3.3.1 → pyft8-3.6.1}/tests/misc/bits_loopback.py +0 -0
  33. {pyft8-3.3.1 → pyft8-3.6.1}/tests/misc/bits_loopback_short.py +0 -0
  34. {pyft8-3.3.1 → pyft8-3.6.1}/tests/misc/osd.py +0 -0
  35. {pyft8-3.3.1 → pyft8-3.6.1}/tests/misc/symbols_loopback.py +0 -0
  36. {pyft8-3.3.1 → pyft8-3.6.1}/tests/misc/test_generate_wav.py +0 -0
  37. {pyft8-3.3.1 → pyft8-3.6.1}/tests/misc/test_loopback_performance.py +0 -0
  38. {pyft8-3.3.1 → pyft8-3.6.1}/tests/misc/view_worked_before.py +0 -0
  39. {pyft8-3.3.1 → pyft8-3.6.1}/tests/pipeline/Fine tuning compare IOS_PC.py +0 -0
  40. {pyft8-3.3.1 → pyft8-3.6.1}/tests/pipeline/Targetted tests.py +0 -0
  41. {pyft8-3.3.1 → pyft8-3.6.1}/tests/pipeline/test_01.wav +0 -0
  42. {pyft8-3.3.1 → pyft8-3.6.1}/tests/plot_baseline.py +0 -0
  43. {pyft8-3.3.1 → pyft8-3.6.1}/tests/spare.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: PyFT8
3
- Version: 3.3.1
3
+ Version: 3.6.1
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
@@ -51,8 +51,7 @@ If you're interested in how this works, maybe have a look at [MiniPyFT8](https:/
51
51
  - Number of remote stations hearing your Tx, number of remote Txs that you're hearing, plus the same info for the 'best' station in your level 4 square
52
52
  - Data used for the above is cached to disk so is not lost when restarting the program
53
53
 
54
- <img width="997" height="1025" alt="screenshot" src="https://github.com/user-attachments/assets/88228f65-c39e-45ee-9f73-1d85138563f3" />
55
-
54
+ <img width="983" height="1021" alt="screenshot" src="https://github.com/user-attachments/assets/591b4423-cf50-4be7-88c1-a9ba2347a326" />
56
55
 
57
56
  ## Motivation
58
57
  This started out as me thinking "How hard can it be, really?" after some frustration with Windows moving sound devices around and wanting to get a minimal decoder running that I can fully control.
@@ -1,4 +1,4 @@
1
- import threading, os, pickle, json, time
1
+ import threading, os, pickle, json
2
2
  from PyFT8.pskreporter import PSKR_MQTT_listener
3
3
  from PyFT8.time_utils import time_utils
4
4
 
@@ -85,8 +85,9 @@ class DiskDict:
85
85
  return
86
86
 
87
87
  class History:
88
- def __init__(self, config_folder, my_call, home_square, pskr_refresh_mins):
88
+ def __init__(self, config_folder, my_call, home_square = '', geo_units = 'km', pskr_refresh_mins = 20):
89
89
  self.pskr_refresh_mins = pskr_refresh_mins
90
+ self.geo_units = geo_units
90
91
  self.log_cache = None
91
92
  self.my_call = my_call
92
93
  self.home_square = home_square
@@ -116,8 +117,8 @@ class History:
116
117
  c, b, m = key_parts
117
118
  if m == 'FT8':
118
119
  #print(f"Parsing adif log: Add hearing & heard by {c} on {b}")
119
- self.add_myspots_record(self.hearing_me.data, None, b, c, 0, 0)
120
- self.add_myspots_record(self.heard_by_me.data, None, b, c, 0, 0)
120
+ self._add_myspots_record(self.hearing_me.data, None, b, c, 0, 0)
121
+ self._add_myspots_record(self.heard_by_me.data, None, b, c, 0, 0)
121
122
  else:
122
123
  print(m)
123
124
 
@@ -136,7 +137,7 @@ class History:
136
137
  call = r['call_b'] if TxRx == 'Rx' else r['call_a']
137
138
  if len(call) > 3:
138
139
  data = self.heard_by_me.data if TxRx == 'Rx' else self.hearing_me.data
139
- self.add_myspots_record(data, None, band, call, 0, 0)
140
+ self._add_myspots_record(data, None, band, call, 0, 0)
140
141
 
141
142
  def parse_all_txt(self):
142
143
  rows, recs = None, []
@@ -154,9 +155,22 @@ class History:
154
155
  print("Didn't find any records in an ALL.txt file in the config folder")
155
156
  return recs
156
157
 
157
- def write_all_txt_row(self, cyclestart_string, fMHz, TxRx, mode, snr, dt, fHz, msg):
158
+ def process_message(self, message, band_info, myCall):
159
+ m = message
160
+ self._write_all_txt_row(m['cyclestart_string'], float(band_info['fMHz']), 'Rx', 'FT8',
161
+ m['their_snr'], m['dt'], m['fHz'], m['hail'], m['their_call'], m['grid_rpt'])
162
+ self._add_myspots_record(self.heard_by_me.data, self.heard_by_me_new,
163
+ band_info['current_band'], m['their_call'], int(time_utils.time()), m['their_snr'])
164
+ if m['hail'] == myCall: # They're calling me, so they're hearing me
165
+ rpt = m['grid_rpt'][-3:]
166
+ if rpt.isnumeric():
167
+ self._add_myspots_record(self.hearing_me.data, self.hearing_me_new,
168
+ band_info['current_band'], m['their_call'], int(time_utils.time()), int(rpt))
169
+
170
+
171
+ def _write_all_txt_row(self, cyclestart_string, fMHz, TxRx, mode, snr_str, dt, fHz, hail, their_call, grid_rpt):
158
172
  filemode = 'w' if not os.path.exists(self.all_file) else 'a'
159
- row = f"{cyclestart_string} {fMHz:8.3f} {TxRx} {mode} {snr:+03d} {dt:4.1f} {fHz:4.0f} {msg}"
173
+ row = f"{cyclestart_string} {fMHz:8.3f} {TxRx} {mode} {snr_str} {dt:4.1f} {fHz:4.0f} {hail} {their_call} {grid_rpt}"
160
174
  with open(self.all_file, filemode) as f:
161
175
  f.write(f"{row}\n")
162
176
 
@@ -169,9 +183,9 @@ class History:
169
183
  if self.home_square_lev4 in grid:
170
184
  self.add_homespots_record(f"{d['b']}_{iTxRx}_{call}", tnow)
171
185
  if d['sc'] == self.my_call:
172
- self.add_myspots_record(self.hearing_me.data, self.hearing_me_new, d['b'], d['rc'], tnow, d['rp'])
186
+ self._add_myspots_record(self.hearing_me.data, self.hearing_me_new, d['b'], d['rc'], tnow, d['rp'])
173
187
  if d['rc'] == self.my_call:
174
- self.add_myspots_record(self.heard_by_me.data, self.heard_by_me_new, d['b'], d['sc'], tnow, d['rp'])
188
+ self._add_myspots_record(self.heard_by_me.data, self.heard_by_me_new, d['b'], d['sc'], tnow, d['rp'])
175
189
 
176
190
  def store_best_grid(self, call, grid):
177
191
  if call.startswith('<'): return
@@ -183,7 +197,7 @@ class History:
183
197
  self.band_TxRx_homecall_recent_L4grid.data.setdefault(key, [])
184
198
  self.band_TxRx_homecall_recent_L4grid.data[key].append(int(t))
185
199
 
186
- def add_myspots_record(self, historic_data, new_alert_data, band, call, t, rp):
200
+ def _add_myspots_record(self, historic_data, new_alert_data, band, call, t, rp):
187
201
  self._update_new_alert(band, call, historic_data, new_alert_data)
188
202
  historic_data.setdefault(band, {})
189
203
  if call in historic_data[band]:
@@ -259,12 +273,19 @@ class History:
259
273
  self.dist_brg_cache.setdefault(grid, grids_to_dist_brg(self.home_square, grid, units))
260
274
  return self.dist_brg_cache[grid]
261
275
 
262
- def get_geo_text(self, call, units):
276
+ def get_message_extra_info(self, their_call, current_band):
277
+ geo_text = self.get_geo_text(their_call)
278
+ wb_time = self.log_cache.get(their_call,'')
279
+ wb_text = f"wb: {time_utils.format_duration(time_utils.time() - float(wb_time))}" if wb_time else ''
280
+ hearing_me = '# ' if self.is_hearing_me(current_band, their_call) else ' '
281
+ return hearing_me, wb_text, geo_text
282
+
283
+ def get_geo_text(self, call):
263
284
  geo_text = ''
264
285
  grid = self.call_to_grid.data.get(call, False)
265
286
  if grid:
266
- loc = grid if units == 'grid' else self.get_dist_brg(grid, units)
267
- units_str = '' if units == 'grid' else ('km' if 'km' in units else 'mi')
287
+ loc = grid if self.geo_units == 'grid' else self.get_dist_brg(grid, self.geo_units)
288
+ units_str = '' if self.geo_units == 'grid' else ('km' if 'km' in self.geo_units else 'mi')
268
289
  geo_text = f"{int(loc[0]):5d}{units_str} {int(loc[1]):3d}°"
269
290
  return geo_text
270
291
 
@@ -294,7 +315,7 @@ class ADIF:
294
315
  self.cache[cbm] = tm
295
316
 
296
317
  def _build_cache(self):
297
- import calendar
318
+ import calendar, time
298
319
  def parse(rec, field):
299
320
  p = rec.find(field)
300
321
  if p<0:
@@ -0,0 +1,298 @@
1
+ import numpy as np
2
+ import matplotlib.pyplot as plt
3
+ from matplotlib import rcParams
4
+ import queue
5
+ from matplotlib.widgets import Button
6
+ from PyFT8.time_utils import time_utils
7
+
8
+ rcParams['toolbar'] = 'None'
9
+ MAIN_TEXT_COLOR = '#f0f9fa'
10
+ TEXT_BACKGROUND_COLOR = '#2a2b2b'
11
+ INFO_TEXT_COLOR = 'white'
12
+ BUTTONCOLOR = 'grey'
13
+ HOVERCOLOR = 'darkgreen'
14
+ ACTIVE_BUTTON_COLOR = 'cyan'
15
+ INACTIVE_BUTTON_COLOR = '#edeef0'
16
+ MAX_FONT_SIZE_MAIN = 10
17
+ L = {'pmargin':0.04, 'sidebar_width': 0.17, 'banner_height':0.1, 'vsep1':0.01, 'hsep1':0.02}
18
+
19
+
20
+ class ButtonBox:
21
+ def __init__(self, fig, _reset_axis, box, btn_pc = 30, onclick = None, clickargs=None, btn_text = ' ', info_text = ' '):
22
+ self._reset_axis = _reset_axis
23
+ self.clickargs = clickargs
24
+ btnbox, infobox = box.copy(), box.copy()
25
+ btnbox[2] = box[2] * btn_pc /100
26
+ infobox[2] = box[2] * (100-btn_pc) /100
27
+ infobox[0] = box[0] + box[2] * (btn_pc /100)
28
+ self.btn_axs = fig.add_axes(btnbox)
29
+ self.btn_widg = Button(self.btn_axs, btn_text, color = BUTTONCOLOR, hovercolor = HOVERCOLOR)
30
+ self.btn_widg.on_clicked(lambda x: onclick(clickargs))
31
+ self.info_axs = fig.add_axes(infobox)
32
+ self.info_text_inst = self.info_axs.text(0.03, 0.5, info_text, color = INFO_TEXT_COLOR, verticalalignment = 'center', clip_on = True)
33
+ self.set_info_text(info_text)
34
+ self.state_is_active = None
35
+ self.set_state(False)
36
+
37
+ def set_state(self, is_active: bool):
38
+ if is_active != self.state_is_active:
39
+ self.state_is_active = is_active
40
+ color = ACTIVE_BUTTON_COLOR if is_active else INACTIVE_BUTTON_COLOR
41
+ self.btn_widg.label.set_color(color)
42
+ self.btn_axs.draw_artist(self.btn_widg.label)
43
+ if self.info_text_inst:
44
+ self.info_text_inst.set_color(color)
45
+ self.info_axs.draw_artist(self.info_text_inst)
46
+
47
+ def set_info_text(self, info_text):
48
+ self._reset_axis(self.info_axs)
49
+ self.info_text_inst.set_text(info_text)
50
+ self.info_axs.draw_artist(self.info_text_inst)
51
+
52
+
53
+ MESSAGE_TYPES = {'generic':{'bg':'blue', 'fg':'white', 'alpha':0.5}, 'CQ':{'bg':'green', 'fg':'white', 'alpha':0.8},
54
+ 'from_me': {'bg':'yellow', 'fg':'black', 'alpha':0.95}, 'to_me':{'bg':'red', 'fg':'white', 'alpha':0.9}}
55
+ class Msg_box:
56
+ def __init__(self, fig, ax, w, h, onclick):
57
+ from matplotlib.patches import Rectangle
58
+ self.onclick = onclick
59
+ self.fig, self.ax = fig, ax
60
+ rect = Rectangle((0, 0), w, h, edgecolor='lime', lw=2)
61
+ self.patch = self.ax.add_patch(rect)
62
+ self.text_inst = self.ax.text(0, 0, '', fontsize='small', fontweight = 'bold' )
63
+ self.cid = fig.canvas.mpl_connect('button_press_event', self._onclick)
64
+ self.active = False
65
+
66
+ def set_properties(self, x, y, message):
67
+ self.patch.set_xy((x, y))
68
+ self.text_inst.set_position((x, y+1))
69
+ self.text_inst.set_text(message['display_text'])
70
+ message_type_params = MESSAGE_TYPES[message['message_type']]
71
+ self.text_inst.set_color(message_type_params['fg'])
72
+ self.patch.set_facecolor(message_type_params['bg'])
73
+ self.patch.set_alpha(message_type_params['alpha'])
74
+ self.message = message
75
+ self.cycle = message['their_tx_cycle']
76
+ self.active = True
77
+
78
+ def _onclick(self, event):
79
+ if self.active:
80
+ b, _ = self.patch.contains(event)
81
+ if(b):
82
+ self.onclick({'action': 'MESSAGE_CLICK', 'message':self.message})
83
+
84
+ class Gui:
85
+ def __init__(self, message_broker, rig_control, console_print, configured_bands, hearing_me_since_mins = 5):
86
+ self.hearing_me_since_mins = hearing_me_since_mins
87
+ self.waterfall_data = message_broker.waterfall_data
88
+ self.history = message_broker.history
89
+ self.qso_manager = None
90
+ self.configured_bands = configured_bands
91
+ self.console_print = console_print
92
+ self.myCall, self.myGrid = message_broker.myCall, message_broker.myGrid
93
+ self.band_info = {'current_band': None, 'fMHz':0, 'time_set':0}
94
+
95
+ self.plt = plt
96
+ self.fig = plt.figure(figsize = (10,10), facecolor=(.18, .71, .71, 0.4))
97
+ self.fig.canvas.manager.set_window_title('PyFT8 by G1OJS')
98
+ self.wf_top = 1-L['pmargin']-L['banner_height']-L['vsep1']
99
+ self.wf_left = L['pmargin']+L['sidebar_width']+L['hsep1']
100
+
101
+ self.ax_wf = self.fig.add_axes([self.wf_left, L['pmargin'], 1-self.wf_left-L['pmargin'], self.wf_top-L['pmargin']])
102
+ self.ax_wf.set_xticks([])
103
+ self.ax_wf.set_yticks([])
104
+
105
+ self.image = self.ax_wf.imshow(self.waterfall_data['data'],vmax=120,vmin=90,origin='lower',interpolation='none', aspect = 'auto')
106
+ self.ax_ss = self.fig.add_axes([L['pmargin'], self.wf_top+L['vsep1'], L['sidebar_width'], L['banner_height']])
107
+ self.ax_cs = self.fig.add_axes([self.wf_left, self.wf_top+L['vsep1'], 1-self.wf_left-L['pmargin'], L['banner_height']])
108
+
109
+ self.console_rows = [('','white'),('','white'),('','white'),('','white'),('','white')]
110
+ self.hearing_page = 0
111
+ self.active_msg_boxes = []
112
+ self.inactive_msg_boxes = []
113
+ self.button_boxes = []
114
+
115
+ bh, bs = 0.02, 0.002
116
+ bb = ButtonBox(self.fig, self._reset_axis, [L['pmargin'], self.wf_top - (len(self.button_boxes)+1) * bh + bs, L['sidebar_width'], bh-bs], btn_pc = 100,
117
+ btn_text = "CQ", onclick = self._on_click_local, clickargs = {'action':'CQ'})
118
+ self.button_boxes.append(bb)
119
+ bb = ButtonBox(self.fig, self._reset_axis, [L['pmargin'], self.wf_top - (len(self.button_boxes)+1) * bh + bs, L['sidebar_width'], bh-bs], btn_pc = 100,
120
+ btn_text = "Repeat last", onclick = self._on_click_local, clickargs = {'action':'RPT_LAST'})
121
+ self.button_boxes.append(bb)
122
+ bb = ButtonBox(self.fig, self._reset_axis, [L['pmargin'], self.wf_top - (len(self.button_boxes)+1) * bh + bs, L['sidebar_width'], bh-bs], btn_pc = 100,
123
+ btn_text = "Tx off", onclick = self._on_click_local, clickargs = {'action':'TX_OFF'})
124
+ self.button_boxes.append(bb)
125
+ for band_info in self.configured_bands:
126
+ band, fMHz = band_info['band'], band_info['fMHz']
127
+ bb = ButtonBox(self.fig, self._reset_axis, [L['pmargin'], self.wf_top - (len(self.button_boxes)+1) * bh + bs, L['sidebar_width'], bh-bs], btn_pc = 30,
128
+ btn_text = band, onclick = self._on_click_local, clickargs = {'action':'SET_BAND', 'band':band, 'fMHz':fMHz})
129
+ self.button_boxes.append(bb)
130
+
131
+ hm_top = self.wf_top - (len(self.button_boxes)+2) * bh + bs - L['vsep1']
132
+ self.ax_hm = self.fig.add_axes([L['pmargin'], L['pmargin'], L['sidebar_width'], hm_top])
133
+
134
+ self.tlast = 0
135
+ self.band_info = {'current_band': None, 'fMHz':0, 'time_set':0}
136
+ self._refresh_hearing()
137
+ self._refresh_band_buttons()
138
+ self._refresh_square_stats()
139
+
140
+ def get_band_info(self):
141
+ return self.band_info
142
+
143
+ def register_qso_manager(self, qsm):
144
+ self.qso_manager = qsm
145
+
146
+ def set_bandstats_title(self, txt):
147
+ self.ax_ss.set_title(txt, fontsize = 10)
148
+
149
+ def update_console(self, text, color, nlines = 5):
150
+ ax = self.ax_cs
151
+ bbox = ax.get_window_extent().transformed(self.fig.dpi_scale_trans.inverted())
152
+ rowheight = bbox.height * self.fig.dpi / nlines
153
+ fontsize = np.min([0.5 * rowheight, MAX_FONT_SIZE_MAIN])
154
+ self._reset_axis(ax)
155
+ self.console_rows[1:] = self.console_rows[:-1]
156
+ self.console_rows[0] = (text, color)
157
+ for i, rw in enumerate(self.console_rows):
158
+ ax.draw_artist(self._text_row(ax, 0.03, .03+0.9*i/len(self.console_rows), rw[0], color = rw[1], fontsize = fontsize))
159
+
160
+ def monitor_waterfall(self):
161
+ last_ptr = 0
162
+ self.plt.show(block = False)
163
+ t0 = time_utils.time()
164
+ while True:
165
+ time_utils.sleep(0.05)
166
+ self.image.set_data(self.waterfall_data['data'])
167
+ self.ax_wf.draw_artist(self.image)
168
+ for mb in self.active_msg_boxes:
169
+ self.ax_wf.draw_artist(mb.patch)
170
+ self.ax_wf.draw_artist(mb.text_inst)
171
+ self.fig.canvas.update()
172
+ self.fig.canvas.flush_events()
173
+ t = time_utils.time()
174
+ #print(f"{t - t0:6.3f}")
175
+ t0 = t
176
+
177
+ def after_search(self, curr_cycle):
178
+ self._refresh_hearing()
179
+ self._clear_msg_boxes(curr_cycle)
180
+ self._refresh_band_buttons()
181
+ self._refresh_square_stats()
182
+
183
+ def display_message(self, message):
184
+ x = int(message['t0'] / self.waterfall_data['dt'] + message['their_tx_cycle'] * self.waterfall_data['pixels_per_cycle'])
185
+ y = int(message['fHz'] / self.waterfall_data['df'])
186
+ mb = self._get_msg_box()
187
+ self.active_msg_boxes.append(mb)
188
+ mb.set_properties(x, y, message)
189
+
190
+ def _reset_axis(self, ax):
191
+ from matplotlib.patches import Rectangle
192
+ rect = Rectangle((0, 0), 1, 1, facecolor = TEXT_BACKGROUND_COLOR)
193
+ ax.draw_artist(ax.add_patch(rect))
194
+ ax.set_xticks([])
195
+ ax.set_yticks([])
196
+
197
+ def _on_click_local(self, clickargs):
198
+ btn_action = clickargs['action']
199
+ if(btn_action == 'SET_BAND'):
200
+ current_band, freqMHz = clickargs['band'], clickargs['fMHz']
201
+ self.band_info = {'current_band':current_band, 'fMHz':freqMHz, 'time_set':time_utils.time()}
202
+ self.console_print(f"[PyFT8] Set band: {self.band_info['current_band']} {self.band_info['fMHz']}")
203
+ self._refresh_hearing()
204
+ for cyc in range(2):
205
+ self._clear_msg_boxes(cyc)
206
+ self._refresh_band_buttons()
207
+ self._refresh_square_stats()
208
+ if clickargs['action'] == "MESSAGE_CLICK":
209
+ m = clickargs['message']
210
+ m_string = f"{m['hail']} {m['their_call']} {m['grid_rpt']}"
211
+ self.console_print(f"[GUI] Clicked on message '{m_string}'")
212
+ self.qso_manager.on_click(clickargs)
213
+
214
+ def _clear_msg_boxes(self, curr_cycle):
215
+ to_remove = [mb for mb in self.active_msg_boxes if mb.cycle == curr_cycle]
216
+ for mb in to_remove:
217
+ mb.active = False
218
+ self.inactive_msg_boxes.append(mb)
219
+ self.active_msg_boxes.remove(mb)
220
+
221
+ def _get_msg_box(self):
222
+ if len(self.inactive_msg_boxes) == 0:
223
+ mb = Msg_box(self.fig, self.ax_wf, self.waterfall_data['sig_w'], self.waterfall_data['sig_h'], onclick = self._on_click_local)
224
+ else:
225
+ mb = self.inactive_msg_boxes.pop()
226
+ return mb
227
+
228
+ def _text_row(self, ax, x, y, text = '', color = 'white', **args):
229
+ art = ax.text(x, y, text, color = color)
230
+ art.set_fontfamily('monospace')
231
+ return art
232
+
233
+ def _refresh_square_stats(self):
234
+ ax = self.ax_ss
235
+ self._reset_axis(ax)
236
+ ax.draw_artist(ax.text(-0.2,0.75,'Tx'))
237
+ ax.draw_artist(ax.text(-0.2,0.25,'Rx'))
238
+ current_band = self.band_info['current_band']
239
+ if current_band is not None and self.history is not None:
240
+ if current_band in self.history.home_most_remotes:
241
+ tx_lead, rx_lead = self.history.home_most_remotes[current_band]
242
+ n_spotted, n_spotting = self.history.get_spot_counts(current_band, self.myCall)
243
+ ax.draw_artist(self._text_row(ax, 0.03, .75, f"{self.myCall:<7} {tx_lead[0]:<7}", color = '#ff756b' ))
244
+ ax.draw_artist(self._text_row(ax, 0.03, .6, f"{n_spotting:<7} {tx_lead[1]:<7}", color = '#ff756b' ))
245
+ ax.draw_artist(self._text_row(ax, 0.03, .25, f"{self.myCall:<7} {rx_lead[0]:<7}", color = '#b6f0c6' ))
246
+ ax.draw_artist(self._text_row(ax, 0.03, .1, f"{n_spotted:<7} {rx_lead[1]:<7}", color = '#b6f0c6' ))
247
+
248
+ def _refresh_band_buttons(self):
249
+ current_band = self.band_info['current_band']
250
+ grd = self.myGrid[:4]
251
+ for bb in self.button_boxes:
252
+ button_band = bb.clickargs.get('band',None)
253
+ if button_band is not None:
254
+ bb.set_state(button_band == current_band)
255
+ if button_band in self.history.home_activity:
256
+ cnts = self.history.home_activity[button_band]
257
+ bb.set_info_text(f"{cnts[0]}Tx, {cnts[1]}Rx")
258
+ self.fig.canvas.update()
259
+ self.fig.canvas.flush_events()
260
+
261
+ def _refresh_hearing(self):
262
+ current_band = self.band_info['current_band']
263
+ ax = self.ax_hm
264
+ self._reset_axis(ax)
265
+ line_height = 0.03
266
+ bbox = ax.get_window_extent().transformed(self.fig.dpi_scale_trans.inverted())
267
+ nlines = int(0.9 / line_height)
268
+ row_artists = []
269
+ if current_band is not None and self.history is not None:
270
+ historic_data = self.history.hearing_me.data if self.hearing_page == 1 else self.history.heard_by_me.data
271
+ new_calls_data = self.history.hearing_me_new if self.hearing_page == 1 else self.history.heard_by_me_new
272
+ timewindow_str = f"<{self.hearing_me_since_mins:.0f} mins"
273
+ title_txt = f"Hearing me {timewindow_str}" if self.hearing_page==1 else f"Heard by me {timewindow_str}"
274
+ row_artists.append(self._text_row(ax, 0.03, 1 - line_height, title_txt, color = 'white'))
275
+ if current_band in historic_data:
276
+ tnow = time_utils.time()
277
+ band_rpts = historic_data[current_band]
278
+ calls_now = [call for call in band_rpts if (tnow - band_rpts[call]['t']) < 60*self.hearing_me_since_mins]
279
+ calls_now.sort(key = lambda c: band_rpts[c]['t'], reverse = True)
280
+ subtitle_txt = f"{len(calls_now)}/{len(band_rpts)} now/ever"
281
+ row_artists.append(self._text_row(ax, 0.03, 1 - 2*line_height, subtitle_txt, color = 'white'))
282
+ for i, remote_call in enumerate(calls_now[:nlines - 3]):
283
+ rpt = band_rpts[remote_call]
284
+ row_txt = f"{remote_call:<7} {self.history.get_geo_text(remote_call)}"
285
+ color = 'white' if self.history.is_in_new_alert(current_band, remote_call, new_calls_data) else 'lime'
286
+ row_artists.append(self._text_row(ax, 0.03, 1 - line_height*(i+3.2), row_txt, color = color))
287
+ for row_art in row_artists:
288
+ ax.draw_artist(row_art)
289
+ self.fig.canvas.update()
290
+ self.fig.canvas.flush_events()
291
+ self.hearing_page = (self.hearing_page +1 )%2
292
+
293
+
294
+
295
+
296
+
297
+
298
+
@@ -0,0 +1,70 @@
1
+ import queue, threading
2
+ from PyFT8.time_utils import time_utils
3
+
4
+ class Broker():
5
+ def __init__(self):
6
+ self.myCall, self.myGrid = None, None
7
+ self.rx = None
8
+ self.history = None
9
+ self.gui = None
10
+ self.history = None
11
+ self.pskr_upload = None
12
+ self.message_queue = queue.Queue()
13
+ self.message_queue_non_time_critical = queue.Queue()
14
+ self.waterfall_data = None
15
+ self.configured_bands = None
16
+ self.on_decode = None
17
+ threading.Thread(target = self._process_message_ntc, daemon = True).start()
18
+
19
+ def register_on_decode(self, func):
20
+ self.on_decode = func
21
+
22
+ def process_message(self, message_dict):
23
+ hail, their_call, grid_rpt = message_dict['hail'], message_dict['their_call'], message_dict['grid_rpt']
24
+ cyclestart_string, their_snr = message_dict['cyclestart_string'], message_dict['their_snr']
25
+ m = message_dict
26
+ screen_format = f"{cyclestart_string} {their_snr} {m['dt']:4.1f} {m['fHz']:4.0f} ~ {hail} {their_call} {grid_rpt}"
27
+ print(f"{screen_format:50s} decoded@ {m['decode_completed']:5.1f}s, dec = {m['decode_status']}")
28
+
29
+ mtype_val = 0 + 1*(their_call == self.myCall) + 2*(hail == self.myCall) + 3*(their_call != self.myCall and hail.startswith('CQ'))
30
+ mtype = ['generic', 'from_me', 'to_me', 'CQ'][mtype_val]
31
+ message_dict.update( {'message_type':mtype, 'display_text':f"{hail} {their_call} {grid_rpt}",
32
+ 'priority':(mtype == 'to_me' or mtype == 'CQ')} )
33
+
34
+ if self.history:
35
+ current_band = self.gui.get_band_info()['current_band']
36
+ hearing_me, wb_text, geo_text = self.history.get_message_extra_info(their_call, current_band)
37
+ display_text = f"{message_dict['hail']} {message_dict['their_call']} {message_dict['grid_rpt']} {hearing_me} {wb_text} {geo_text}"
38
+ message_dict.update( {'hearing_me':hearing_me, 'wb_text':wb_text, 'geo_text':geo_text, 'display_text':display_text } )
39
+ if message_dict['priority']:
40
+ if self.gui:
41
+ self.gui.display_message(message_dict)
42
+
43
+ self.message_queue_non_time_critical.put(message_dict)
44
+
45
+ if self.on_decode:
46
+ self.on_decode(message_dict)
47
+
48
+ def _process_message_ntc(self):
49
+ while True:
50
+ time_utils.sleep(0.25)
51
+ while not self.message_queue_non_time_critical.empty():
52
+ time_utils.sleep(0.01)
53
+ m = self.message_queue_non_time_critical.get()
54
+ band_info = None
55
+ if self.gui:
56
+ band_info = self.gui.get_band_info()
57
+ if not m['priority']:
58
+ self.gui.display_message(m)
59
+ if band_info:
60
+ if m['their_call'] != 'not':
61
+ if self.history:
62
+ self.history.process_message(m, band_info, self.myCall)
63
+ if self.pskr_upload:
64
+ if float(band_info['time_set']) < time_utils.time() - 10: # bad QRG Guard
65
+ if m['their_call'] != self.myCall:
66
+ self.pskr_upload.add_report(m['their_call'], int(1000000*float(band_info['fMHz'])) + m['fHz'],
67
+ m['their_snr'], 'FT8', 1, int(time_utils.time()))
68
+
69
+
70
+