PyFT8 2.6.1__tar.gz → 2.7.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 (34) hide show
  1. {pyft8-2.6.1 → pyft8-2.7.0}/PKG-INFO +3 -3
  2. pyft8-2.7.0/PyFT8/gui.py +222 -0
  3. pyft8-2.7.0/PyFT8/maidenhead.py +32 -0
  4. pyft8-2.7.0/PyFT8/mqtt.py +133 -0
  5. {pyft8-2.6.1 → pyft8-2.7.0}/PyFT8/pyft8.py +73 -45
  6. {pyft8-2.6.1 → pyft8-2.7.0}/PyFT8.egg-info/PKG-INFO +3 -3
  7. {pyft8-2.6.1 → pyft8-2.7.0}/PyFT8.egg-info/SOURCES.txt +1 -0
  8. {pyft8-2.6.1 → pyft8-2.7.0}/README.md +2 -2
  9. {pyft8-2.6.1 → pyft8-2.7.0}/pyproject.toml +1 -1
  10. pyft8-2.6.1/PyFT8/gui.py +0 -153
  11. pyft8-2.6.1/PyFT8/mqtt.py +0 -84
  12. {pyft8-2.6.1 → pyft8-2.7.0}/LICENSE +0 -0
  13. {pyft8-2.6.1 → pyft8-2.7.0}/MANIFEST.in +0 -0
  14. {pyft8-2.6.1 → pyft8-2.7.0}/PyFT8/__init__.py +0 -0
  15. {pyft8-2.6.1 → pyft8-2.7.0}/PyFT8/callhashes.py +0 -0
  16. {pyft8-2.6.1 → pyft8-2.7.0}/PyFT8/hamlib.py +0 -0
  17. {pyft8-2.6.1 → pyft8-2.7.0}/PyFT8/pskr_upload.py +0 -0
  18. {pyft8-2.6.1 → pyft8-2.7.0}/PyFT8/receiver.py +0 -0
  19. {pyft8-2.6.1 → pyft8-2.7.0}/PyFT8/rigctrl.py +0 -0
  20. {pyft8-2.6.1 → pyft8-2.7.0}/PyFT8/time_utils.py +0 -0
  21. {pyft8-2.6.1 → pyft8-2.7.0}/PyFT8/transmitter.py +0 -0
  22. {pyft8-2.6.1 → pyft8-2.7.0}/PyFT8.egg-info/dependency_links.txt +0 -0
  23. {pyft8-2.6.1 → pyft8-2.7.0}/PyFT8.egg-info/entry_points.txt +0 -0
  24. {pyft8-2.6.1 → pyft8-2.7.0}/PyFT8.egg-info/requires.txt +0 -0
  25. {pyft8-2.6.1 → pyft8-2.7.0}/PyFT8.egg-info/top_level.txt +0 -0
  26. {pyft8-2.6.1 → pyft8-2.7.0}/setup.cfg +0 -0
  27. {pyft8-2.6.1 → pyft8-2.7.0}/tests/dev/CQ AAAA.py +0 -0
  28. {pyft8-2.6.1 → pyft8-2.7.0}/tests/dev/osd.py +0 -0
  29. {pyft8-2.6.1 → pyft8-2.7.0}/tests/dev/test_generate_wav.py +0 -0
  30. {pyft8-2.6.1 → pyft8-2.7.0}/tests/dev/test_loopback_performance.py +0 -0
  31. {pyft8-2.6.1 → pyft8-2.7.0}/tests/dev/view_worked_before.py +0 -0
  32. {pyft8-2.6.1 → pyft8-2.7.0}/tests/plot_baseline.py +0 -0
  33. {pyft8-2.6.1 → pyft8-2.7.0}/tests/spare.py +0 -0
  34. {pyft8-2.6.1 → pyft8-2.7.0}/tests/test_batch_and_live.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: PyFT8
3
- Version: 2.6.1
3
+ Version: 2.7.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
@@ -41,7 +41,7 @@ If you're interested in how this works, maybe have a look at [MiniPyFT8](https:/
41
41
  The Gui shows:
42
42
  - Simultaneous views of odd and even cycles
43
43
  - Messages overlaid on waterfall signals that produce them
44
- - Worked-before info and fine grid locators in the message boxes (distance and bearing coming soon)
44
+ - Worked-before info and fine grid locators / distance and bearing in the message boxes
45
45
  - Band activity on band select buttons
46
46
  - 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
47
47
 
@@ -50,8 +50,8 @@ To enable uploading of spots to pskreporter, make sure that your .ini file inclu
50
50
  [pskreporter]
51
51
  upload = Y
52
52
  ```
53
- <img width="1078" height="436" alt="Capture" src="https://github.com/user-attachments/assets/5c4b64cb-e3e6-4a2f-aa77-93fd361a4e74" />
54
53
 
54
+ <img width="980" height="807" alt="screenshot" src="https://github.com/user-attachments/assets/ac393a05-277a-4d98-bd74-78bcb0ae8b03" />
55
55
 
56
56
 
57
57
  ## Motivation
@@ -0,0 +1,222 @@
1
+ import numpy as np
2
+ import matplotlib.pyplot as plt
3
+ import time, queue
4
+ from matplotlib import rcParams
5
+ from matplotlib.animation import FuncAnimation
6
+ from matplotlib.widgets import Slider, Button
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.16, 'banner_height':0.1, 'vsep1':0.01, 'hsep1':0.02}
18
+
19
+ # ================== WATERFALL ======================================================
20
+
21
+ class Scrollbox:
22
+ def __init__(self, fig, box, nlines = 5, monospace = False, default_text = '', fontsize = None):
23
+ self.fig = fig
24
+ self.ax = fig.add_axes(box)
25
+ self.default_text = default_text
26
+ bbox = self.ax.get_window_extent().transformed(fig.dpi_scale_trans.inverted())
27
+ self.fontsize = np.min([0.5 * bbox.height * fig.dpi / nlines, MAX_FONT_SIZE_MAIN]) if fontsize is None else fontsize
28
+ self.nlines = nlines
29
+ self.line_height = 0.9 / nlines
30
+ self.lines = []
31
+ self.lineartists = []
32
+ for i in range(self.nlines):
33
+ self.lineartists.append(self.ax.text(0.03,1 - self.line_height * (i+1),
34
+ '', color = MAIN_TEXT_COLOR, fontsize = self.fontsize))
35
+ if monospace:
36
+ self.lineartists[-1].set_fontfamily('monospace')
37
+ self.ax.set_xticks([])
38
+ self.ax.set_yticks([])
39
+ self.ax.set_facecolor(TEXT_BACKGROUND_COLOR)
40
+
41
+ def scroll_print(self, text, color = MAIN_TEXT_COLOR):
42
+ self.lines = self.lines[-(self.nlines-1):]
43
+ self.lines.append({'text':text, 'color':color})
44
+ for i, line in enumerate(self.lines):
45
+ self.lineartists[i].set_text(line['text'])
46
+ self.lineartists[i].set_color(line['color'])
47
+
48
+ def clear(self):
49
+ self.lines = []
50
+ for i in range(self.nlines):
51
+ self.lineartists[i].set_text(self.default_text)
52
+
53
+ def list_print(self, lst):
54
+ self.lines = [{'text':l, 'color':'white'} for l in lst[:self.nlines]]
55
+ for i, line in enumerate(self.lines):
56
+ if line['text'] != self.lineartists[i].get_text():
57
+ self.lineartists[i].set_text(line['text'])
58
+ self.lineartists[i].set_color(line['color'])
59
+ for i in range(len(lst), self.nlines):
60
+ if self.lineartists[i].get_text() != '':
61
+ self.lineartists[i].set_text('')
62
+
63
+
64
+ class Msg_box:
65
+ def __init__(self, fig, ax, tbin, fbin, w, h, onclick):
66
+ from matplotlib.patches import Rectangle
67
+ self.onclick = onclick
68
+ rect = Rectangle((tbin, fbin), width=w, height=h, alpha=0.6, edgecolor='lime', lw=2)
69
+ self.patch = ax.add_patch(rect)
70
+ self.text_inst = ax.text(tbin, fbin+2, '', fontsize='small', fontweight='bold' )
71
+ self.cid = fig.canvas.mpl_connect('button_press_event', self._onclick)
72
+ self.expire = 0
73
+
74
+ def set_properties(self, message):
75
+ self.message = message
76
+ self.patch.set_x(message.h0_idx)
77
+ self.text_inst.set_x(message.h0_idx)
78
+ self.patch.set_visible(True)
79
+ self.text_inst.set_visible(True)
80
+ self.expire = message.expire
81
+
82
+ def set_appearance(self, message):
83
+ self.text_inst.set_text(message.gui_text)
84
+ colors = ['blue', 'white']
85
+ if message.is_cq: colors = ['green', 'white']
86
+ if message.is_from_me: colors = ['yellow', 'white']
87
+ if message.is_to_me: colors = ['red', 'white']
88
+ self.text_inst.set_color(colors[1])
89
+ self.patch.set_facecolor(colors[0])
90
+
91
+ def hide_if_expired(self):
92
+ if time.time() > self.expire > 0:
93
+ self.patch.set_visible(False)
94
+ self.text_inst.set_visible(False)
95
+
96
+ def _onclick(self, event):
97
+ b, _ = self.patch.contains(event)
98
+ if(b):
99
+ self.onclick(self.message)
100
+
101
+ class ButtonBox:
102
+ def __init__(self, fig, box, btn_pc = 30, onclick = None, clickargs=None, btn_label = ''):
103
+ btnbox, infobox = box.copy(), box.copy()
104
+ btnbox[2] = box[2] * btn_pc /100
105
+ infobox[2] = box[2] * (100-btn_pc) /100
106
+ infobox[0] = box[0] + box[2] * (btn_pc /100)
107
+ self.btn_axs = fig.add_axes(btnbox)
108
+ self.info_axs = fig.add_axes(infobox)
109
+ self.info_axs.set_xticks([])
110
+ self.info_axs.set_yticks([])
111
+ self.info_axs.set_facecolor(TEXT_BACKGROUND_COLOR)
112
+ self.label2 = self.info_axs.text(0.03, 0.5, '', color = INFO_TEXT_COLOR, verticalalignment = 'center')
113
+ self.btn_widg = Button(self.btn_axs, btn_label, color = BUTTONCOLOR, hovercolor = HOVERCOLOR)
114
+ self.label = self.btn_widg.label
115
+ self.label.set_color(MAIN_TEXT_COLOR)
116
+ self.clickargs = clickargs
117
+ self.active = False
118
+ self.btn_widg.on_clicked(lambda x: onclick(clickargs))
119
+
120
+ def set_active(self, active: bool):
121
+ if self.active != active:
122
+ self.active = active
123
+ self._update_appearance()
124
+
125
+ def set_info_text(self, text):
126
+ self.label2.set_text(text)
127
+
128
+ def get_info_text(self):
129
+ return self.label2
130
+
131
+ def _update_appearance(self):
132
+ color = ACTIVE_BUTTON_COLOR if self.active else INACTIVE_BUTTON_COLOR
133
+ self.label.set_color(color)
134
+ self.label2.set_color(color)
135
+
136
+ class Gui:
137
+ def __init__(self, dBgrid, hps, bpt, config, on_gui_sidebars_refresh, on_msg_click, on_control_click):
138
+ if config is not None:
139
+ self.mStation = {'c':config['station']['call'], 'g':config['station']['grid']}
140
+ self.on_msg_click = on_msg_click
141
+ self.on_control_click = on_control_click
142
+ self.on_gui_sidebars_refresh = on_gui_sidebars_refresh
143
+ self.dBgrid = dBgrid
144
+ self.hps, self.bpt = hps, bpt
145
+ self.msg_boxes = {}
146
+ self.decode_queue = queue.Queue()
147
+ self.make_layout(config)
148
+ self.ani = FuncAnimation(self.fig, self._animate, interval = 40, frames=(100000), blit=True)
149
+
150
+ def make_layout(self, config):
151
+ # figure
152
+ self.plt = plt
153
+ self.fig = plt.figure(figsize = (10,10), facecolor=(.18, .71, .71, 0.4))
154
+ self.fig.canvas.manager.set_window_title('PyFT8 by G1OJS')
155
+ wf_top = 1-L['pmargin']-L['banner_height']-L['vsep1']
156
+ wf_left = L['pmargin']+L['sidebar_width']+L['hsep1']
157
+
158
+ # waterfall
159
+ self.ax_wf = self.fig.add_axes([wf_left, L['pmargin'], 1-wf_left-L['pmargin'], wf_top-L['pmargin']])
160
+ self.image = self.ax_wf.imshow(self.dBgrid.T,vmax=120,vmin=90,origin='lower',interpolation='none', aspect = 'auto')
161
+ self.ax_wf.set_xticks([])
162
+ self.ax_wf.set_yticks([])
163
+
164
+ # band stats
165
+ self.band_stats = Scrollbox(self.fig, [L['pmargin'], wf_top+L['vsep1'], L['sidebar_width'], L['banner_height']], nlines = 4, monospace = True)
166
+ self.band_stats.ax.text(-0.2,0.75,'Tx')
167
+ self.band_stats.ax.text(-0.2,0.25,'Rx')
168
+ self.band_stats.ax.set_title(f"Spots to/from {config['station']['grid'][:4]}", fontsize = 10)
169
+
170
+ # console
171
+ self.console = Scrollbox(self.fig, [wf_left, wf_top+L['vsep1'], 1-wf_left-L['pmargin'], L['banner_height']])
172
+
173
+ # control buttons
174
+ self.button_boxes = []
175
+ bh, bs = 0.02, 0.002
176
+ bb = ButtonBox(self.fig, [L['pmargin'], wf_top - (len(self.button_boxes)+1) * bh + bs, L['sidebar_width'], bh-bs], btn_pc = 100,
177
+ btn_label = "CQ", onclick = self.on_control_click, clickargs = {'action':'CQ'})
178
+ self.button_boxes.append(bb)
179
+ bb = ButtonBox(self.fig, [L['pmargin'], wf_top - (len(self.button_boxes)+1) * bh + bs, L['sidebar_width'], bh-bs], btn_pc = 100,
180
+ btn_label = "Repeat last", onclick = self.on_control_click, clickargs = {'action':'RPT_LAST'})
181
+ self.button_boxes.append(bb)
182
+ bb = ButtonBox(self.fig, [L['pmargin'], wf_top - (len(self.button_boxes)+1) * bh + bs, L['sidebar_width'], bh-bs], btn_pc = 100,
183
+ btn_label = "Tx off", onclick = self.on_control_click, clickargs = {'action':'TX_OFF'})
184
+ self.button_boxes.append(bb)
185
+ for band, freq in config['bands'].items():
186
+ bb = ButtonBox(self.fig, [L['pmargin'], wf_top - (len(self.button_boxes)+1) * bh + bs, L['sidebar_width'], bh-bs], btn_pc = 30,
187
+ btn_label = band, onclick = self.on_control_click, clickargs = {'action':'SET_BAND','band':band,'freq':freq})
188
+ self.button_boxes.append(bb)
189
+
190
+ # hearing me list
191
+ self.hm = Scrollbox(self.fig, [L['pmargin'], L['pmargin'], L['sidebar_width'], wf_top - (len(self.button_boxes)+2) * bh + bs - L['vsep1']],
192
+ nlines = 30, monospace = True, fontsize = 8)
193
+
194
+
195
+ def refresh_sidebars(self):
196
+ self.on_gui_sidebars_refresh(self)
197
+
198
+ def add_message_box(self, message):
199
+ self.decode_queue.put(message)
200
+
201
+ def _display_message_box(self, message):
202
+ h0_idx, f0_idx = message.h0_idx, message.f0_idx
203
+ if not f0_idx in self.msg_boxes:
204
+ self.msg_boxes[f0_idx] = Msg_box(self.fig, self.ax_wf, h0_idx, f0_idx, 79*self.hps, 8*self.bpt, onclick = self.on_msg_click)
205
+ self.msg_boxes[f0_idx].set_properties(message)
206
+ self.msg_boxes[f0_idx].set_appearance(message)
207
+
208
+ def _tidy_msg_boxes(self):
209
+ for fb in self.msg_boxes:
210
+ self.msg_boxes[fb].hide_if_expired()
211
+
212
+ def _animate(self, frame):
213
+ self.image.set_data(self.dBgrid.T)
214
+ while not self.decode_queue.empty():
215
+ self._display_message_box(self.decode_queue.get())
216
+ if (frame % 10 == 0):
217
+ self._tidy_msg_boxes()
218
+ self.refresh_sidebars()
219
+ return [self.image, *self.ax_wf.patches, *self.ax_wf.texts, *self.band_stats.lineartists, *self.console.lineartists, *self.hm.lineartists,
220
+ *[bb.label for bb in self.button_boxes], *[bb.label2 for bb in self.button_boxes]]
221
+
222
+
@@ -0,0 +1,32 @@
1
+ import numpy as np
2
+
3
+ def grid_to_latlong(grid, centre = True):
4
+ lat, lon = -90, -180
5
+ grid = grid.upper()
6
+ if centre:
7
+ grid = grid + "LL44LL44LL44"[len(grid):]
8
+ mults = [20, 2, 2/24, 0.2/24, 0.2/(24*24), 0.02/(24*24)]
9
+ grid = grid[:2*len(mults)]
10
+ pairs = [grid[i:i+2] for i in range(0,len(grid),2)]
11
+ for i, p in enumerate(pairs):
12
+ zero = [ord('A'),ord('0')][i % 2]
13
+ lon += mults[i] * (ord(p[0]) - zero)
14
+ lat += mults[i] * (ord(p[1]) - zero) / 2
15
+ return (lat, lon)
16
+
17
+
18
+ def db(sq1, sq2):
19
+ ll1, ll2 = grid_to_latlong(sq1), grid_to_latlong(sq2)
20
+ lats = [np.radians(ll1[0]),np.radians(ll2[0])]
21
+ dlat, dlon = np.radians(ll2[0] - ll1[0]), np.radians(ll2[1] - ll1[1])
22
+ s_lats, c_lats = np.sin(lats), np.cos(lats)
23
+ a = np.sin(dlat/2)**2 + c_lats[0] * c_lats[1] * np.sin(dlon/2)**2
24
+ r = 6371 * 2 * np.asin(np.sqrt(a))
25
+ b = np.atan2(c_lats[1] * np.sin(dlon), c_lats[0] * s_lats[1] - s_lats[0] * c_lats[1] * np.cos(dlon))
26
+ return (r, np.degrees(b) % 360)
27
+
28
+ #grids = ['LL44LL44LL44', 'IO90', 'IO90JU', 'IO90JU44', 'IO90JU95LX', 'IO90JU96MA','IN79jx55']
29
+ #for g in grids:
30
+ # print(g, grid_to_latlong(g, centre = True))
31
+
32
+ #print(db('IO90','IO91'))
@@ -0,0 +1,133 @@
1
+ import paho.mqtt.client as mqtt
2
+ import threading
3
+ import time
4
+ from ast import literal_eval
5
+
6
+ SPOTLIFE = 15*60
7
+
8
+ import os
9
+ import pickle
10
+
11
+ class DiskDict:
12
+ def __init__(self, file):
13
+ self.file = file
14
+ self.data = {}
15
+ self.load()
16
+ threading.Thread(target = self._autosave, daemon = True).start()
17
+
18
+ def _autosave(self, autosave_period = 15):
19
+ while True:
20
+ time.sleep(autosave_period)
21
+ self.save()
22
+
23
+ def load(self):
24
+ if(os.path.exists(self.file)):
25
+ with open(f"{self.file}","rb") as f:
26
+ self.data = pickle.load(f)
27
+
28
+ def save(self):
29
+ if self.data:
30
+ with open(f"{self.file}","wb") as f:
31
+ pickle.dump(self.data, f)
32
+
33
+ class PSKR_MQTT_listener:
34
+ def __init__(self, config_folder, my_call, home_square):
35
+ self.my_call = my_call
36
+ self.hearing_me = DiskDict(f"{config_folder}/hearing_me.pkl")
37
+ self.home_square = home_square
38
+ self.callsign_cache = DiskDict(f"{config_folder}/callsign_cache.pkl")
39
+ self.band_TxRx_homecall_report_times = DiskDict(f"{config_folder}/report_times.pkl")
40
+ self.band_TxRx_homecall_countremotes = {}
41
+ self.home_activity = {}
42
+ self.home_most_remotes = {}
43
+ self.lock = threading.Lock()
44
+ mqttc = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2)
45
+ mqttc.on_connect = self.on_connect
46
+ mqttc.on_message = self.on_message
47
+ try:
48
+ mqttc.connect("mqtt.pskreporter.info", 1883, 60)
49
+ except:
50
+ print("[MQTT] connection error")
51
+ threading.Thread(target = mqttc.loop_forever, daemon = True).start()
52
+ threading.Thread(target = self.count_activity, daemon = True).start()
53
+
54
+ def on_connect(self, client, userdata, flags, reason_code, properties):
55
+ #pskr/filter/v2/{band}/{mode}/{sendercall}/{receivercall}/{senderlocator}/{receiverlocator}/{sendercountry}/{receivercountry}
56
+ print(f"[MQTT] Requesting mqtt feed for {self.home_square}")
57
+ client.subscribe(f"pskr/filter/v2/+/FT8/+/+/{self.home_square}/#")
58
+ client.subscribe(f"pskr/filter/v2/+/FT8/+/+/+/{self.home_square}/#")
59
+
60
+ def on_message(self, client, userdata, msg):
61
+ try:
62
+ d = literal_eval(msg.payload.decode())
63
+ except:
64
+ return
65
+ sc, rc = (d['sc'], d['sl']), (d['rc'], d['rl'])
66
+ for i, c in enumerate([sc, rc]):
67
+ call, loc = c
68
+ if call not in self.callsign_cache.data:
69
+ self.callsign_cache.data[call] = loc
70
+ if self.home_square in loc:
71
+ key = f"{d['b']}_{['Tx','Rx'][i]}_{call}"
72
+ if not key in self.band_TxRx_homecall_report_times.data:
73
+ with self.lock:
74
+ self.band_TxRx_homecall_report_times.data[key] = []
75
+ self.band_TxRx_homecall_report_times.data[key].append(time.time())
76
+ if d['sc'] == self.my_call:
77
+ if d['b'] not in self.hearing_me.data:
78
+ self.hearing_me.data[d['b']] = {}
79
+ if d['rc'] not in self.hearing_me.data[d['b']]:
80
+ self.hearing_me.data[d['b']][d['rc']] = {'t':time.time(), 'rp':d['rp'], 'c':d['rc']}
81
+
82
+ def count_activity(self):
83
+ import numpy as np
84
+ while True:
85
+ time.sleep(5)
86
+ self.home_activity = {}
87
+ self.band_TxRx_homecall_countremotes = {}
88
+ self.home_most_remotes = {}
89
+ with self.lock:
90
+ for band_TxRx_homecall in self.band_TxRx_homecall_report_times.data:
91
+ b = band_TxRx_homecall.split("_")[0]
92
+ self.home_activity[b] = [0, 0]
93
+
94
+ for band_TxRx_homecall in self.band_TxRx_homecall_report_times.data:
95
+ band_TxRx_homecall_report_times = self.band_TxRx_homecall_report_times.data[band_TxRx_homecall]
96
+ band_TxRx_homecall_report_times = [t for t in band_TxRx_homecall_report_times if (time.time() - t) < SPOTLIFE]
97
+ self.band_TxRx_homecall_report_times.data[band_TxRx_homecall] = band_TxRx_homecall_report_times
98
+
99
+ for band_TxRx_homecall in self.band_TxRx_homecall_report_times.data:
100
+ band_TxRx_homecall_report_times = self.band_TxRx_homecall_report_times.data[band_TxRx_homecall]
101
+ if len(band_TxRx_homecall_report_times):
102
+ b, tr, c = band_TxRx_homecall.split("_")
103
+ self.home_activity[b][['Tx','Rx'].index(tr)] +=1
104
+ nremotes = len(band_TxRx_homecall_report_times)
105
+ if not b in self.home_most_remotes:
106
+ self.home_most_remotes[b] = [('',0), ('',0)]
107
+ if nremotes>self.home_most_remotes[b][['Tx','Rx'].index(tr)][1]:
108
+ self.home_most_remotes[b][['Tx','Rx'].index(tr)] = (c, nremotes)
109
+
110
+ for b in self.hearing_me.data:
111
+ newdict = {}
112
+ for c in self.hearing_me.data[b]:
113
+ if (time.time() - self.hearing_me.data[b][c]['t']) < SPOTLIFE:
114
+ newdict[c] = self.hearing_me.data[b][c]
115
+ self.hearing_me.data[b] = newdict
116
+
117
+ def get_spot_counts(self, band, call):
118
+ tx_reports = self.band_TxRx_homecall_report_times.data.get(f"{band}_Tx_{call}", [])
119
+ rx_reports = self.band_TxRx_homecall_report_times.data.get(f"{band}_Rx_{call}", [])
120
+ n_spotting = len(tx_reports) if tx_reports else 0
121
+ n_spotted = len(rx_reports) if rx_reports else 0
122
+ return n_spotted, n_spotting
123
+
124
+ if __name__ == '__main__':
125
+ pskr = PSKR_MQTT_listener("IO90")
126
+
127
+
128
+
129
+
130
+
131
+
132
+
133
+
@@ -12,11 +12,13 @@ from PyFT8.time_utils import global_time_utils
12
12
  from PyFT8.rigctrl import Rig
13
13
  from PyFT8.hamlib import Rig_hamlib
14
14
  from PyFT8.mqtt import PSKR_MQTT_listener
15
+ import PyFT8.maidenhead as maidenhead
15
16
 
16
- VER = '2.6.1'
17
+ VER = '2.7.0'
17
18
 
18
19
  MAX_TX_START_SECONDS = 2.5
19
20
  rig, gui, qso, adif_logging, pskr_info, pskr_upload = None, None, None, None, None, None
21
+ busy_profile, hearing_me = None, None
20
22
 
21
23
  def get_config():
22
24
  import configparser
@@ -88,6 +90,16 @@ class ADIF:
88
90
  cache[c + "_"+b+"_FT8"] = tm
89
91
  return cache
90
92
 
93
+ def get_geo_text(call):
94
+ geo_text = ''
95
+ loc = pskr_info.callsign_cache.data.get(call,'')
96
+ if loc and config['gui']['loc'] == 'km_deg':
97
+ loc = maidenhead.db(config['station']['grid'], loc)
98
+ geo_text = f"{int(loc[0]):5d}k {int(loc[1]):3d}°"
99
+ if loc and config['gui']['loc'] == 'loc':
100
+ geo_text = f"loc: {loc}"
101
+ return geo_text
102
+
91
103
  class Message:
92
104
  def __init__(self, candidate):
93
105
  c = candidate
@@ -100,12 +112,10 @@ class Message:
100
112
  self.is_from_me = c.msg_tuple[1] == mycall
101
113
  self.is_to_me = c.msg_tuple[0] == mycall
102
114
  self.is_cq = c.msg_tuple[0].startswith('CQ')
103
- call = c.msg_tuple[1]
104
- loc = pskr_info.cache.get(call,'')
105
- qui_loc_text = f"loc: {loc}" if loc else ''
106
- wb = adif_logging.cache.get(call,'')
107
- gui_wb_text = f"wb: {global_time_utils.format_duration(time.time() - float(wb))}" if wb else ''
108
- self.gui_text = f"{c.msg} {gui_wb_text} {qui_loc_text}"
115
+ geo_text = get_geo_text(c.msg_tuple[1])
116
+ wb_time = adif_logging.cache.get(c.msg_tuple[1],'')
117
+ wb_text = f"wb: {global_time_utils.format_duration(time.time() - float(wb_time))}" if wb_time else ''
118
+ self.gui_text = f"{c.msg} {wb_text} {geo_text}"
109
119
 
110
120
  def wsjtx_screen_format(self):
111
121
  return f"{self.cyclestart['string']} {self.snr:+03d} {self.dt:4.1f} {self.fHz:4.0f} ~ {self.msg}"
@@ -152,7 +162,7 @@ class FT8_QSO:
152
162
  time.sleep(delay)
153
163
  if self.tx_cycle is None:
154
164
  self.tx_cycle = global_time_utils.curr_cycle_from_time()
155
- self.tx_freq = clear_frequencies[self.tx_cycle]
165
+ self.tx_freq = clearest_frequency
156
166
  console_print(f"[PyFT8] Set tx cycle = {self.tx_cycle} f = {self.tx_freq:5.1f}")
157
167
  symbols = audio_out.create_ft8_symbols(self.message_to_transmit)
158
168
  if any(symbols):
@@ -252,42 +262,59 @@ def on_rx_decode(c):
252
262
  print(message.wsjtx_screen_format())
253
263
  write_all_txt_row(message)
254
264
 
255
- def on_rx_busy_profile(busy_profile, cycle):
265
+ def on_rx_busy_profile(busy_profile_new, cycle):
266
+ global busy_profile
256
267
  if output_device_idx is None:
257
268
  return
258
- fmax = 950 if qso.band_info['b']=='60m' else 2000
259
- f0_idx, fn_idx = int(500/audio_in.df), int(fmax/audio_in.df)
260
- idx = np.argmin(busy_profile[f0_idx:fn_idx])
261
- clear_frequencies[cycle] = (f0_idx + idx) * audio_in.df
262
- console_print(f"[on_busy] Set Tx freq to {clear_frequencies[cycle]:6.1f} for cycle {cycle}")
269
+ if busy_profile is not None:
270
+ busy_profile += busy_profile_new
271
+ fmax = 950 if qso.band_info['b']=='60m' else 2000
272
+ f0_idx, fn_idx = int(500/audio_in.df), int(fmax/audio_in.df)
273
+ idx = np.argmin(busy_profile[f0_idx:fn_idx])
274
+ clearest_frequency = (f0_idx + idx) * audio_in.df
275
+ busy_profile = busy_profile_new
276
+ #console_print(f"[on_busy] Set Tx freq to {clearest_frequency:6.1f}")
263
277
 
264
278
  #============= Callbacks for GUI ==========================================================
265
- def gui_update_usermessages():
279
+ def on_gui_sidebars_refresh(gui):
266
280
  if qso.band_info['b'] is None:
267
281
  console_print(f"[PyFT8] Band not set; please select a band.", color = 'red')
268
- if pskr_info is not None and gui is not None:
269
- grd = config['station']['grid'][:4]
270
- #s = [f"{b} {cnts[0]}/{cnts[1]} " for b, cnts in pskr_info.home_activity.items()]
271
- #console_print(f"Tx/Rx calls in {grd}: {' '.join(s)}", color = 'yellow')
272
- for bw in gui.buttons:
273
- band_text = bw.user_data['label']
274
- if band_text in pskr_info.home_activity:
275
- cnts = pskr_info.home_activity[band_text]
276
- activity_text = f"{cnts[0]}t/{cnts[1]}r"
277
- bw.label.set_text(f"{band_text} {activity_text}")
278
-
279
- b = qso.band_info['b']
280
- if b is not None and b in pskr_info.home_most_remotes:
281
- tx_lead, rx_lead = pskr_info.home_most_remotes[b]
282
- call = config['station']['call']
283
- n_spotted, n_spotting = pskr_info.get_spot_counts(b, call)
284
- gui.band_stats.print(f"{call:<7} {tx_lead[0]:<7}", color = '#ff756b')
285
- gui.band_stats.print(f"{n_spotting:<7} {tx_lead[1]:<7}", color = '#ff756b')
286
- gui.band_stats.print(f"{call:<7} {rx_lead[0]:<7}", color = '#b6f0c6')
287
- gui.band_stats.print(f"{n_spotted:<7} {rx_lead[1]:<7}", color = '#b6f0c6')
282
+ if pskr_info is None:
283
+ return
284
+
285
+ # refresh band stats
286
+ grd = config['station']['grid'][:4]
287
+ for bb in gui.button_boxes:
288
+ band = bb.clickargs.get('band','')
289
+ if band:
290
+ bb.set_active(band == qso.band_info.get('b',''))
291
+ if band in pskr_info.home_activity:
292
+ cnts = pskr_info.home_activity[band]
293
+ new_text = f"{cnts[0]}Tx, {cnts[1]}Rx"
294
+ if new_text != bb.get_info_text():
295
+ bb.set_info_text(new_text)
288
296
 
289
- def on_gui_control_click(btn_widg):
290
- btn_def = btn_widg.user_data
297
+ # refresh home square counts
298
+ b = qso.band_info['b']
299
+ if b is not None and b in pskr_info.home_most_remotes:
300
+ tx_lead, rx_lead = pskr_info.home_most_remotes[b]
301
+ call = config['station']['call']
302
+ n_spotted, n_spotting = pskr_info.get_spot_counts(b, call)
303
+ # add local count here for n_spotted prior to round trip to pskreporter?
304
+ gui.band_stats.scroll_print(f"{call:<7} {tx_lead[0]:<7}", color = '#ff756b')
305
+ gui.band_stats.scroll_print(f"{n_spotting:<7} {tx_lead[1]:<7}", color = '#ff756b')
306
+ gui.band_stats.scroll_print(f"{call:<7} {rx_lead[0]:<7}", color = '#b6f0c6')
307
+ gui.band_stats.scroll_print(f"{n_spotted:<7} {rx_lead[1]:<7}", color = '#b6f0c6')
308
+
309
+ #refresh hearing me
310
+ hearing_me_text = []
311
+ if b is not None and b in pskr_info.hearing_me.data:
312
+ for h in pskr_info.hearing_me.data[b].values():
313
+ geo_text = geo_text = get_geo_text(h['c'])
314
+ hearing_me_text.append(f"{h['c']:<7} {int(h['rp']):+03d} {geo_text:<12}")
315
+ gui.hm.list_print(['Hearing me:'] + hearing_me_text)
316
+
317
+ def on_gui_control_click(btn_def):
291
318
  btn_action = btn_def['action']
292
319
  if btn_action == "CQ":
293
320
  mc, mg = config['station']['call'], config['station']['grid'][:4]
@@ -298,25 +325,26 @@ def on_gui_control_click(btn_widg):
298
325
  console_print("[PyFT8] Set PTT Off")
299
326
  rig.ptt_off()
300
327
  qso.tx_cycle = None
301
- if(btn_action == 'SET_FREQ'):
302
- band, freqMHz = btn_def['label'], btn_def['data']
328
+ if(btn_action == 'SET_BAND'):
329
+ band, freqMHz = btn_def['band'], btn_def['freq']
303
330
  qso.band_info = {'b':band, 'fMHz':freqMHz}
304
331
  rig.set_freq_Hz(int(1000000*float(qso.band_info['fMHz'])))
305
332
  console_print(f"[PyFT8] Set band: {qso.band_info['b']} {qso.band_info['fMHz']}")
306
333
  gui.band_stats.clear()
307
-
334
+ gui.refresh_sidebars()
335
+
308
336
  def on_gui_msg_click(message):
309
337
  progress_qso(message)
310
338
 
311
339
  #=============== CLI ========================================================================
312
340
  def console_print(text, color = 'white'):
313
341
  if gui is not None:
314
- gui.console.print(text, color)
342
+ gui.console.scroll_print(text, color)
315
343
  else:
316
344
  print(text)
317
345
 
318
346
  def cli():
319
- global audio_in, audio_out, output_device_idx, rig, gui, qso, config, config_folder, clear_frequencies, adif_logging, pskr_upload, pskr_info
347
+ global audio_in, audio_out, output_device_idx, rig, gui, qso, config, config_folder, clearest_frequency, adif_logging, pskr_upload, pskr_info
320
348
  import time
321
349
  parser = argparse.ArgumentParser(prog='PyFT8rx', description = 'Command Line FT8 decoder')
322
350
  parser.add_argument('-c', '--config_folder', help = 'Location of config folder e.g. C:/Users/drala/Documents/Projects/GitHub/G1OJS/PyFT8_cfg', default = './')
@@ -336,7 +364,7 @@ def cli():
336
364
  if mc is not None and 'pskreporter' in config.keys():
337
365
  if config['pskreporter']['upload'] == 'Y':
338
366
  pskr_upload = PSKR_upload(mc, mg, software = f"PyFT8 v{VER}", console_print = console_print) if not mc is None else None
339
- pskr_info = PSKR_MQTT_listener(mg[:4])
367
+ pskr_info = PSKR_MQTT_listener(config_folder, mc, mg[:4])
340
368
  qso = FT8_QSO()
341
369
  if config.has_section('hamlib_rig'):
342
370
  console_print("Connecting to rig via Hamlib")
@@ -347,7 +375,7 @@ def cli():
347
375
 
348
376
  if args.transmit_message or args.outputcard_keywords:
349
377
  audio_out = AudioOut()
350
- clear_frequencies = [760, 760]
378
+ clearest_frequency = 760
351
379
 
352
380
  if args.outputcard_keywords:
353
381
  outputcard_keywords = args.outputcard_keywords.replace(' ','').split(',')
@@ -364,7 +392,7 @@ def cli():
364
392
  if not input_device_idx:
365
393
  console_print("No input device")
366
394
  else:
367
- gui = None if args.no_gui else Gui(audio_in.dBgrid_main, 4, 2, config, gui_update_usermessages, on_gui_msg_click, on_gui_control_click)
395
+ gui = None if args.no_gui else Gui(audio_in.dBgrid_main, 4, 2, config, on_gui_sidebars_refresh, on_gui_msg_click, on_gui_control_click)
368
396
  rx = Receiver(audio_in, [200, 3100], on_rx_decode, on_rx_busy_profile)
369
397
  audio_in.start_streamed_audio(input_device_idx)
370
398
  if gui is not None:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: PyFT8
3
- Version: 2.6.1
3
+ Version: 2.7.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
@@ -41,7 +41,7 @@ If you're interested in how this works, maybe have a look at [MiniPyFT8](https:/
41
41
  The Gui shows:
42
42
  - Simultaneous views of odd and even cycles
43
43
  - Messages overlaid on waterfall signals that produce them
44
- - Worked-before info and fine grid locators in the message boxes (distance and bearing coming soon)
44
+ - Worked-before info and fine grid locators / distance and bearing in the message boxes
45
45
  - Band activity on band select buttons
46
46
  - 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
47
47
 
@@ -50,8 +50,8 @@ To enable uploading of spots to pskreporter, make sure that your .ini file inclu
50
50
  [pskreporter]
51
51
  upload = Y
52
52
  ```
53
- <img width="1078" height="436" alt="Capture" src="https://github.com/user-attachments/assets/5c4b64cb-e3e6-4a2f-aa77-93fd361a4e74" />
54
53
 
54
+ <img width="980" height="807" alt="screenshot" src="https://github.com/user-attachments/assets/ac393a05-277a-4d98-bd74-78bcb0ae8b03" />
55
55
 
56
56
 
57
57
  ## Motivation
@@ -6,6 +6,7 @@ PyFT8/__init__.py
6
6
  PyFT8/callhashes.py
7
7
  PyFT8/gui.py
8
8
  PyFT8/hamlib.py
9
+ PyFT8/maidenhead.py
9
10
  PyFT8/mqtt.py
10
11
  PyFT8/pskr_upload.py
11
12
  PyFT8/pyft8.py
@@ -22,7 +22,7 @@ If you're interested in how this works, maybe have a look at [MiniPyFT8](https:/
22
22
  The Gui shows:
23
23
  - Simultaneous views of odd and even cycles
24
24
  - Messages overlaid on waterfall signals that produce them
25
- - Worked-before info and fine grid locators in the message boxes (distance and bearing coming soon)
25
+ - Worked-before info and fine grid locators / distance and bearing in the message boxes
26
26
  - Band activity on band select buttons
27
27
  - 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
28
28
 
@@ -31,8 +31,8 @@ To enable uploading of spots to pskreporter, make sure that your .ini file inclu
31
31
  [pskreporter]
32
32
  upload = Y
33
33
  ```
34
- <img width="1078" height="436" alt="Capture" src="https://github.com/user-attachments/assets/5c4b64cb-e3e6-4a2f-aa77-93fd361a4e74" />
35
34
 
35
+ <img width="980" height="807" alt="screenshot" src="https://github.com/user-attachments/assets/ac393a05-277a-4d98-bd74-78bcb0ae8b03" />
36
36
 
37
37
 
38
38
  ## Motivation
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "PyFT8"
3
- version = "2.6.1"
3
+ version = "2.7.0"
4
4
  license = "GPL-3.0-or-later"
5
5
 
6
6
  authors = [
pyft8-2.6.1/PyFT8/gui.py DELETED
@@ -1,153 +0,0 @@
1
- import numpy as np
2
- import matplotlib.pyplot as plt
3
- import time, queue
4
- from matplotlib import rcParams
5
- from matplotlib.animation import FuncAnimation
6
- from matplotlib.widgets import Slider, Button
7
-
8
- rcParams['toolbar'] = 'None'
9
- # ================== WATERFALL ======================================================
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, family="monospace"))
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
-
34
- def clear(self):
35
- for i in range(self.nlines):
36
- self.print("")
37
-
38
- class Msg_box:
39
- def __init__(self, fig, ax, tbin, fbin, w, h, onclick):
40
- from matplotlib.patches import Rectangle
41
- self.onclick = onclick
42
- rect = Rectangle((tbin, fbin), width=w, height=h, alpha=0.6, edgecolor='lime', lw=2)
43
- self.patch = ax.add_patch(rect)
44
- self.text_inst = ax.text(tbin, fbin+2, '', fontsize='small', fontweight='bold' )
45
- self.cid = fig.canvas.mpl_connect('button_press_event', self._onclick)
46
- self.expire = 0
47
-
48
- def set_properties(self, message):
49
- self.message = message
50
- self.patch.set_x(message.h0_idx)
51
- self.text_inst.set_x(message.h0_idx)
52
- self.patch.set_visible(True)
53
- self.text_inst.set_visible(True)
54
- self.expire = message.expire
55
-
56
- def set_appearance(self, message):
57
- self.text_inst.set_text(message.gui_text)
58
- colors = ['blue', 'white']
59
- if message.is_cq: colors = ['green', 'white']
60
- if message.is_from_me: colors = ['yellow', 'black']
61
- if message.is_to_me: colors = ['red', 'white']
62
- self.text_inst.set_color(colors[1])
63
- self.patch.set_facecolor(colors[0])
64
-
65
- def hide_if_expired(self):
66
- if time.time() > self.expire > 0:
67
- self.patch.set_visible(False)
68
- self.text_inst.set_visible(False)
69
-
70
- def _onclick(self, event):
71
- b, _ = self.patch.contains(event)
72
- if(b):
73
- self.onclick(self.message)
74
-
75
- class Gui:
76
- def __init__(self, dBgrid, hps, bpt, config, update_usermessages, on_msg_click, on_control_click):
77
- if config is not None:
78
- self.mStation = {'c':config['station']['call'], 'g':config['station']['grid']}
79
- self.on_msg_click = on_msg_click
80
- self.on_control_click = on_control_click
81
- self.update_usermessages = update_usermessages
82
- self.dBgrid = dBgrid
83
- self.hps, self.bpt = hps, bpt
84
- self.msg_boxes = {}
85
- self.decode_queue = queue.Queue()
86
- self.pmarg = 0.04
87
- self.make_layout(config)
88
- self.ani = FuncAnimation(self.fig, self._animate, interval = 40, frames=(100000), blit=True)
89
-
90
- def make_layout(self, config, wf_left = 0.15, wf_top = 0.87, left_width = 0.13):
91
- self.plt = plt
92
- self.fig = plt.figure(figsize = (10,10), facecolor=(.18, .71, .71, 0.4))
93
- self.fig.canvas.manager.set_window_title('PyFT8 by G1OJS')
94
- self.ax_wf = self.fig.add_axes([self.pmarg + wf_left, self.pmarg, 1-2*self.pmarg-wf_left, wf_top-self.pmarg])
95
- self.image = self.ax_wf.imshow(self.dBgrid.T,vmax=120,vmin=90,origin='lower',interpolation='none', aspect = 'auto')
96
- self.ax_wf.set_xticks([])
97
- self.ax_wf.set_yticks([])
98
- self.sep_h = 0.002
99
- self.ax_band_stats = self.fig.add_axes([self.pmarg, wf_top + self.sep_h, left_width, 1-self.pmarg - (wf_top + self.sep_h)])
100
- self.band_stats = Scrollbox(self.fig, self.ax_band_stats, nlines = 4)
101
- self.ax_band_stats.text(-0.2,0.75,'Tx')
102
- self.ax_band_stats.text(-0.2,0.25,'Rx')
103
- if 'station' in config:
104
- self.ax_band_stats.set_title(f"Spots to/from {config['station']['grid'][:4]}", fontsize = 10)
105
- self.ax_console = self.fig.add_axes([self.pmarg + wf_left, wf_top, 1-2*self.pmarg - wf_left, 1-self.pmarg-wf_top])
106
- self.console = Scrollbox(self.fig, self.ax_console)
107
-
108
- if config is not None:
109
- styles = {'ctrl':{'fc':'grey','c':'black'}, 'band':{'fc':'green','c':'white'}}
110
- button_defs = [{'label':'CQ','style':'ctrl','action':'CQ', 'data':None}, {'label':'Repeat last','style':'ctrl','action':'RPT_LAST','data':None},
111
- {'label':'Tx off','style':'ctrl','action':'TX_OFF', 'data':None}]
112
- #{'label':'Averaging','style':'ctrl','data':None}]
113
- for band, freq in config['bands'].items():
114
- button_defs.append({'label':band,'style':'band','action':'SET_FREQ','data':freq})
115
- self._make_buttons(button_defs, styles, wf_top, 0.02, left_width, 0.002)
116
-
117
- def _make_buttons(self, btn_defs, styles, btns_top, btn_h, btn_w, sep_h):
118
- self.buttons = []
119
- for i, btn_def in enumerate(btn_defs):
120
- btn_axs = plt.axes([self.pmarg, btns_top - (i+1) * btn_h, btn_w, btn_h-sep_h])
121
- style = styles[btn_def['style']]
122
- btn_widg = Button(btn_axs, '', color=style['fc'], hovercolor='skyblue')
123
- btn_widg.user_data = btn_def
124
- btn_widg.label = btn_axs.text(0.05, 0.5, btn_def['label'], verticalalignment='center', horizontalalignment='left',
125
- color = 'white', fontweight = 'bold', transform=btn_axs.transAxes)
126
- btn_widg.on_clicked(lambda event, btn_widg=btn_widg: self.on_control_click(btn_widg))
127
- self.buttons.append(btn_widg)
128
-
129
- def add_message_box(self, message):
130
- self.decode_queue.put(message)
131
-
132
- def _display_message_box(self, message):
133
- h0_idx, f0_idx = message.h0_idx, message.f0_idx
134
- if not f0_idx in self.msg_boxes:
135
- self.msg_boxes[f0_idx] = Msg_box(self.fig, self.ax_wf, h0_idx, f0_idx, 79*self.hps, 8*self.bpt, onclick = self.on_msg_click)
136
- self.msg_boxes[f0_idx].set_properties(message)
137
- self.msg_boxes[f0_idx].set_appearance(message)
138
-
139
- def _tidy_msg_boxes(self):
140
- for fb in self.msg_boxes:
141
- self.msg_boxes[fb].hide_if_expired()
142
-
143
- def _animate(self, frame):
144
- self.image.set_data(self.dBgrid.T)
145
- while not self.decode_queue.empty():
146
- self._display_message_box(self.decode_queue.get())
147
- if (frame % 10 == 0):
148
- self._tidy_msg_boxes()
149
- if (frame % 50 == 0):
150
- self.update_usermessages()
151
- return [self.image, *self.ax_wf.patches, *self.ax_wf.texts, *self.band_stats.lineartists, *self.console.lineartists, *[btn.label for btn in self.buttons]]
152
-
153
-
pyft8-2.6.1/PyFT8/mqtt.py DELETED
@@ -1,84 +0,0 @@
1
- import paho.mqtt.client as mqtt
2
- import threading
3
- import time
4
- from ast import literal_eval
5
-
6
- class PSKR_MQTT_listener:
7
-
8
- def __init__(self, home_square):
9
- self.home_square = home_square
10
- self.cache = {}
11
- self.band_TxRx_homecall_report_times = {}
12
- self.band_TxRx_homecall_countremotes = {}
13
- self.home_activity = {}
14
- self.home_most_remotes = {}
15
- self.lock = threading.Lock()
16
- mqttc = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2)
17
- mqttc.on_connect = self.on_connect
18
- mqttc.on_message = self.on_message
19
- try:
20
- mqttc.connect("mqtt.pskreporter.info", 1883, 60)
21
- except:
22
- print("[MQTT] connection error")
23
- threading.Thread(target = mqttc.loop_forever, daemon = True).start()
24
- threading.Thread(target = self.count_activity, daemon = True).start()
25
-
26
- def on_connect(self, client, userdata, flags, reason_code, properties):
27
- #pskr/filter/v2/{band}/{mode}/{sendercall}/{receivercall}/{senderlocator}/{receiverlocator}/{sendercountry}/{receivercountry}
28
- print(f"[MQTT] Requesting mqtt feed for {self.home_square}")
29
- client.subscribe(f"pskr/filter/v2/+/FT8/+/+/{self.home_square}/#")
30
- client.subscribe(f"pskr/filter/v2/+/FT8/+/+/+/{self.home_square}/#")
31
-
32
-
33
- def on_message(self, client, userdata, msg):
34
- try:
35
- d = literal_eval(msg.payload.decode())
36
- except:
37
- return
38
- sc, rc = (d['sc'], d['sl']), (d['rc'], d['rl'])
39
- for i, c in enumerate([sc, rc]):
40
- call, loc = c
41
- if call not in self.cache:
42
- self.cache[call] = loc
43
- if self.home_square in loc:
44
- key = f"{d['b']}_{['Tx','Rx'][i]}_{call}"
45
- if not key in self.band_TxRx_homecall_report_times:
46
- with self.lock:
47
- self.band_TxRx_homecall_report_times[key] = []
48
- self.band_TxRx_homecall_report_times[key].append(time.time())
49
-
50
- def count_activity(self):
51
- while True:
52
- time.sleep(5)
53
- self.band_TxRx_homecall_countremotes = {}
54
- with self.lock:
55
- for band_TxRx_homecall in self.band_TxRx_homecall_report_times:
56
- b = band_TxRx_homecall.split("_")[0]
57
- self.home_activity[b] = [0,0]
58
- for band_TxRx_homecall in self.band_TxRx_homecall_report_times:
59
- b, tr, c = band_TxRx_homecall.split("_")
60
- report_times = self.band_TxRx_homecall_report_times[band_TxRx_homecall]
61
- report_times = [t for t in report_times if (time.time() - t) < 15*60]
62
- self.band_TxRx_homecall_report_times[band_TxRx_homecall] = report_times
63
- nremotes = len(report_times)
64
- self.home_activity[b][['Tx','Rx'].index(tr)] +=1
65
- if not b in self.home_most_remotes:
66
- self.home_most_remotes[b] = [('',0), ('',0)]
67
- if nremotes>self.home_most_remotes[b][['Tx','Rx'].index(tr)][1]:
68
- self.home_most_remotes[b][['Tx','Rx'].index(tr)] = (c, nremotes)
69
-
70
- def get_spot_counts(self, band, call):
71
- n_spotting = len(self.band_TxRx_homecall_report_times.get(f"{band}_Tx_{call}", []))
72
- n_spotted = len(self.band_TxRx_homecall_report_times.get(f"{band}_Rx_{call}", []))
73
- return n_spotted, n_spotting
74
-
75
- if __name__ == '__main__':
76
- pskr = PSKR_MQTT_listener("IO90")
77
-
78
-
79
-
80
-
81
-
82
-
83
-
84
-
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes